From b92a91ab37368bee8aeb35ecb468ffbb6b76aa2a Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Nov 2025 12:56:14 +0300 Subject: [PATCH 1/2] Final Update: second variant complete --- control1-2.py | 1302 ++++++++++++++++++++++++++++++++++++++++ control2-2.py | 902 ++++++++++++++++++++++++++++ control2.py | 104 ++-- control2.py.bak | 693 +++++++++++++++++++++ fitness.db | Bin 40960 -> 45056 bytes masterpol.db | Bin 94208 -> 94208 bytes service_requests.db | Bin 36864 -> 36864 bytes service_requests_v2.db | Bin 0 -> 45056 bytes 8 files changed, 2939 insertions(+), 62 deletions(-) create mode 100644 control1-2.py create mode 100644 control2-2.py create mode 100644 control2.py.bak create mode 100644 service_requests_v2.db diff --git a/control1-2.py b/control1-2.py new file mode 100644 index 0000000..360f50d --- /dev/null +++ b/control1-2.py @@ -0,0 +1,1302 @@ +import sys +import sqlite3 +from datetime import datetime +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QTextEdit, QComboBox, QPushButton, QTableWidget, + QTableWidgetItem, QTabWidget, QGroupBox, QMessageBox, QFileDialog, + QSplitter, QHeaderView, QFormLayout, QCheckBox, QDialog, QDateEdit) +from PyQt6.QtCore import Qt, pyqtSignal, QDate +from PyQt6.QtGui import QIcon, QAction +import os + +class DatabaseManager: + def __init__(self): + self.conn = sqlite3.connect('service_requests_v2.db') + self.create_tables() + + def create_tables(self): + cursor = self.conn.cursor() + + # Таблица пользователей + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + role TEXT NOT NULL, + full_name TEXT NOT NULL, + phone TEXT, + email TEXT + ) + ''') + + # Таблица заявок с расширенными полями + cursor.execute(''' + CREATE TABLE IF NOT EXISTS service_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_name TEXT NOT NULL, + client_phone TEXT NOT NULL, + client_email TEXT, + equipment_type TEXT NOT NULL, + equipment_model TEXT NOT NULL, + serial_number TEXT, + problem_description TEXT NOT NULL, + status TEXT DEFAULT 'Новая', + priority TEXT DEFAULT 'Средний', + request_type TEXT DEFAULT 'Ремонт', + assigned_operator TEXT, + assigned_master TEXT, + observer_group TEXT, + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + completed_date TIMESTAMP, + deadline DATE, + marked_for_deletion BOOLEAN DEFAULT 0, + duplicate_of INTEGER + ) + ''') + + # Таблица истории заявок + cursor.execute(''' + CREATE TABLE IF NOT EXISTS request_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + user_id INTEGER, + action TEXT, + details TEXT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Таблица вложений + cursor.execute(''' + CREATE TABLE IF NOT EXISTS attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + filename TEXT, + filepath TEXT, + file_type TEXT, + uploaded_by INTEGER, + upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Таблица отчетов + cursor.execute(''' + CREATE TABLE IF NOT EXISTS reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + master_id INTEGER, + work_description TEXT, + parts_used TEXT, + time_spent REAL, + labor_cost REAL, + parts_cost REAL, + total_cost REAL, + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Таблица заказов запчастей + cursor.execute(''' + CREATE TABLE IF NOT EXISTS part_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + part_name TEXT, + part_number TEXT, + quantity INTEGER, + supplier TEXT, + estimated_cost REAL, + status TEXT DEFAULT 'Заказан', + ordered_by INTEGER, + order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expected_date DATE, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Таблица МТР (материально-технических ресурсов) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS material_needs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + material_name TEXT, + material_type TEXT, + quantity INTEGER, + unit TEXT, + urgency TEXT DEFAULT 'Обычная', + status TEXT DEFAULT 'Требуется', + estimated_cost REAL, + notes TEXT, + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Создаем тестовых пользователей для варианта 2 + self.create_test_users() + + self.conn.commit() + + def create_test_users(self): + cursor = self.conn.cursor() + + test_users = [ + ('operator1', '123', 'operator', 'Петр Петров', '+79167654321', 'operator@company.ru'), + ('master1', '123', 'master', 'Сергей Сергеев', '+79169998877', 'master@company.ru'), + ('admin1', '123', 'admin', 'Администратор Системы', '+79160001122', 'admin@company.ru') + ] + + for user in test_users: + try: + cursor.execute( + 'INSERT INTO users (username, password, role, full_name, phone, email) VALUES (?, ?, ?, ?, ?, ?)', + user + ) + except sqlite3.IntegrityError: + pass # Пользователь уже существует + + self.conn.commit() + +class ServiceRequestAppV2(QMainWindow): + def __init__(self): + super().__init__() + self.db = DatabaseManager() + self.current_user = None + self.init_ui() + + def init_ui(self): + self.setWindowTitle('Система учета заявок на ремонт оргтехники - Вариант 2') + self.setGeometry(100, 100, 1400, 800) + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Основной layout + layout = QVBoxLayout(central_widget) + + # Панель входа + self.login_widget = self.create_login_widget() + layout.addWidget(self.login_widget) + + # Основной интерфейс (скрыт до входа) + self.main_tabs = QTabWidget() + self.main_tabs.setVisible(False) + layout.addWidget(self.main_tabs) + + def create_login_widget(self): + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel('Вход в систему - Вариант 2')) + + form_layout = QFormLayout() + + self.username_input = QLineEdit() + self.password_input = QLineEdit() + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) + + form_layout.addRow('Логин:', self.username_input) + form_layout.addRow('Пароль:', self.password_input) + + layout.addLayout(form_layout) + + login_btn = QPushButton('Войти') + login_btn.clicked.connect(self.login) + layout.addWidget(login_btn) + + # Подсказка с тестовыми пользователями + hint = QLabel('Тестовые пользователи: operator1/123, master1/123, admin1/123') + hint.setStyleSheet('color: gray; font-size: 10px;') + layout.addWidget(hint) + + return widget + + def login(self): + username = self.username_input.text() + password = self.password_input.text() + + cursor = self.db.conn.cursor() + cursor.execute( + 'SELECT * FROM users WHERE username = ? AND password = ?', + (username, password) + ) + + user = cursor.fetchone() + + if user: + self.current_user = { + 'id': user[0], + 'username': user[1], + 'role': user[3], + 'full_name': user[4] + } + self.show_main_interface() + else: + QMessageBox.warning(self, 'Ошибка', 'Неверный логин или пароль') + + def show_main_interface(self): + self.login_widget.setVisible(False) + self.main_tabs.setVisible(True) + + # Очищаем предыдущие вкладки + self.main_tabs.clear() + + role = self.current_user['role'] + + if role == 'operator': + self.setup_operator_interface() + elif role == 'master': + self.setup_master_interface() + elif role == 'admin': + self.setup_admin_interface() + + def setup_operator_interface(self): + # Вкладка регистрации заявок + register_tab = QWidget() + layout = QVBoxLayout(register_tab) + + layout.addWidget(QLabel('Регистрация новой заявки')) + + form_layout = QFormLayout() + + self.op_client_name = QLineEdit() + self.op_client_phone = QLineEdit() + self.op_client_email = QLineEdit() + self.op_equipment_type = QComboBox() + self.op_equipment_type.addItems(['Принтер', 'Копир', 'Сканер', 'МФУ', 'Компьютер', 'Монитор', 'Телефон', 'Другое']) + self.op_equipment_model = QLineEdit() + self.op_serial_number = QLineEdit() + self.op_problem_description = QTextEdit() + + form_layout.addRow('ФИО клиента:', self.op_client_name) + form_layout.addRow('Телефон:', self.op_client_phone) + form_layout.addRow('Email:', self.op_client_email) + form_layout.addRow('Тип оборудования:', self.op_equipment_type) + form_layout.addRow('Модель:', self.op_equipment_model) + form_layout.addRow('Серийный номер:', self.op_serial_number) + form_layout.addRow('Описание проблемы:', self.op_problem_description) + + layout.addLayout(form_layout) + + submit_btn = QPushButton('Зарегистрировать заявку') + submit_btn.clicked.connect(self.register_service_request) + layout.addWidget(submit_btn) + + self.main_tabs.addTab(register_tab, 'Регистрация заявки') + + # Вкладка управления заявками + management_tab = QWidget() + layout = QVBoxLayout(management_tab) + + # Панель фильтров + filter_layout = QHBoxLayout() + filter_layout.addWidget(QLabel('Статус:')) + self.status_filter = QComboBox() + self.status_filter.addItems(['Все', 'Новая', 'В работе', 'Ожидает запчасти', 'Выполнена', 'Отменена']) + filter_layout.addWidget(self.status_filter) + + filter_layout.addWidget(QLabel('Приоритет:')) + self.priority_filter = QComboBox() + self.priority_filter.addItems(['Все', 'Низкий', 'Средний', 'Высокий', 'Критичный']) + filter_layout.addWidget(self.priority_filter) + + filter_btn = QPushButton('Применить фильтры') + filter_btn.clicked.connect(self.load_operator_requests) + filter_layout.addWidget(filter_btn) + + layout.addLayout(filter_layout) + + self.operator_requests_table = QTableWidget() + self.operator_requests_table.setColumnCount(10) + self.operator_requests_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Приоритет', 'Тип', 'Дата', 'Оператор', 'Помечена на удаление' + ]) + layout.addWidget(self.operator_requests_table) + + # Панель управления заявкой + control_group = QGroupBox('Управление заявкой') + control_layout = QHBoxLayout(control_group) + + self.status_combo = QComboBox() + self.status_combo.addItems(['Новая', 'В работе', 'Ожидает запчасти', 'Выполнена', 'Отменена']) + + self.priority_combo = QComboBox() + self.priority_combo.addItems(['Низкий', 'Средний', 'Высокий', 'Критичный']) + + self.type_combo = QComboBox() + self.type_combo.addItems(['Ремонт', 'Обслуживание', 'Консультация', 'Диагностика']) + + self.observer_group = QLineEdit() + self.observer_group.setPlaceholderText('Группа наблюдателей') + + mark_delete_btn = QPushButton('Пометить на удаление') + mark_delete_btn.clicked.connect(self.mark_request_for_deletion) + + update_btn = QPushButton('Обновить заявку') + update_btn.clicked.connect(self.update_request_operator) + + control_layout.addWidget(QLabel('Статус:')) + control_layout.addWidget(self.status_combo) + control_layout.addWidget(QLabel('Приоритет:')) + control_layout.addWidget(self.priority_combo) + control_layout.addWidget(QLabel('Тип:')) + control_layout.addWidget(self.type_combo) + control_layout.addWidget(self.observer_group) + control_layout.addWidget(update_btn) + control_layout.addWidget(mark_delete_btn) + + layout.addWidget(control_group) + + self.main_tabs.addTab(management_tab, 'Управление заявками') + + # Вкладка архива + archive_tab = QWidget() + layout = QVBoxLayout(archive_tab) + + search_layout = QHBoxLayout() + self.archive_search = QLineEdit() + self.archive_search.setPlaceholderText('Поиск в архиве...') + search_btn = QPushButton('Найти') + search_btn.clicked.connect(self.search_archive) + + search_layout.addWidget(self.archive_search) + search_layout.addWidget(search_btn) + layout.addLayout(search_layout) + + self.archive_table = QTableWidget() + self.archive_table.setColumnCount(9) + self.archive_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Дата создания', 'Дата завершения', 'Мастер', 'Тип' + ]) + layout.addWidget(self.archive_table) + + self.main_tabs.addTab(archive_tab, 'Архив') + + self.load_operator_requests() + self.load_archive() + + def setup_master_interface(self): + # Вкладка назначенных заявок + requests_tab = QWidget() + layout = QVBoxLayout(requests_tab) + + self.master_requests_table = QTableWidget() + self.master_requests_table.setColumnCount(8) + self.master_requests_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Приоритет', 'Дата', 'Срок' + ]) + layout.addWidget(self.master_requests_table) + + # Панель управления для мастера + control_group = QGroupBox('Управление ремонтом') + control_layout = QVBoxLayout(control_group) + + # Смена статуса + status_layout = QHBoxLayout() + status_layout.addWidget(QLabel('Статус:')) + self.master_status_combo = QComboBox() + self.master_status_combo.addItems(['В работе', 'Ожидает запчасти', 'Выполнена', 'Отменена']) + status_layout.addWidget(self.master_status_combo) + + update_status_btn = QPushButton('Обновить статус') + update_status_btn.clicked.connect(self.update_request_master) + status_layout.addWidget(update_status_btn) + + control_layout.addLayout(status_layout) + + # Заказ запчастей + parts_group = QGroupBox('Заказ запчастей') + parts_layout = QFormLayout(parts_group) + + self.part_name = QLineEdit() + self.part_number = QLineEdit() + self.part_quantity = QLineEdit() + self.part_quantity.setText('1') + self.part_supplier = QLineEdit() + self.part_cost = QLineEdit() + + parts_layout.addRow('Название запчасти:', self.part_name) + parts_layout.addRow('Номер запчасти:', self.part_number) + parts_layout.addRow('Количество:', self.part_quantity) + parts_layout.addRow('Поставщик:', self.part_supplier) + parts_layout.addRow('Примерная стоимость:', self.part_cost) + + order_parts_btn = QPushButton('Заказать запчасти') + order_parts_btn.clicked.connect(self.order_parts) + parts_layout.addRow(order_parts_btn) + + control_layout.addWidget(parts_group) + + # Прикрепление файлов + file_layout = QHBoxLayout() + attach_photo_btn = QPushButton('Прикрепить фото') + attach_photo_btn.clicked.connect(self.attach_photo) + attach_file_btn = QPushButton('Прикрепить файл') + attach_file_btn.clicked.connect(self.attach_file) + + file_layout.addWidget(attach_photo_btn) + file_layout.addWidget(attach_file_btn) + control_layout.addLayout(file_layout) + + # Создание отчета + report_btn = QPushButton('Создать отчет о выполненной работе') + report_btn.clicked.connect(self.create_report) + control_layout.addWidget(report_btn) + + # История заявки + history_btn = QPushButton('Просмотреть историю заявки') + history_btn.clicked.connect(self.show_request_history) + control_layout.addWidget(history_btn) + + layout.addWidget(control_group) + + self.main_tabs.addTab(requests_tab, 'Мои заявки') + + # Вкладка заказанных запчастей + parts_tab = QWidget() + layout = QVBoxLayout(parts_tab) + + self.parts_table = QTableWidget() + self.parts_table.setColumnCount(8) + self.parts_table.setHorizontalHeaderLabels([ + 'ID', 'Заявка', 'Запчасть', 'Номер', 'Кол-во', 'Статус', 'Дата заказа', 'Поставщик' + ]) + layout.addWidget(self.parts_table) + + self.main_tabs.addTab(parts_tab, 'Заказанные запчасти') + + self.load_master_requests() + self.load_master_parts() + + def setup_admin_interface(self): + # Вкладка управления пользователями + users_tab = QWidget() + layout = QVBoxLayout(users_tab) + + self.users_table = QTableWidget() + self.users_table.setColumnCount(6) + self.users_table.setHorizontalHeaderLabels(['ID', 'Логин', 'ФИО', 'Роль', 'Телефон', 'Email']) + layout.addWidget(self.users_table) + + add_user_btn = QPushButton('Добавить пользователя') + add_user_btn.clicked.connect(self.show_add_user_dialog) + layout.addWidget(add_user_btn) + + self.main_tabs.addTab(users_tab, 'Пользователи') + + # Вкладка всех заявок + requests_tab = QWidget() + layout = QVBoxLayout(requests_tab) + + self.admin_requests_table = QTableWidget() + self.admin_requests_table.setColumnCount(11) + self.admin_requests_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Приоритет', 'Тип', 'Дата', 'Оператор', 'Мастер', 'Помечена на удаление' + ]) + layout.addWidget(self.admin_requests_table) + + delete_btn = QPushButton('Удалить выбранные заявки') + delete_btn.clicked.connect(self.delete_marked_requests) + layout.addWidget(delete_btn) + + self.main_tabs.addTab(requests_tab, 'Все заявки') + + # Вкладка распределения заявок + distribution_tab = QWidget() + layout = QVBoxLayout(distribution_tab) + + self.distribution_table = QTableWidget() + self.distribution_table.setColumnCount(9) + self.distribution_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Статус', 'Оператор', 'Мастер', 'Группа наблюдателей', 'Срок исполнения', 'Приоритет' + ]) + layout.addWidget(self.distribution_table) + + self.main_tabs.addTab(distribution_tab, 'Распределение заявок') + + # Вкладка МТР (материально-технических ресурсов) + materials_tab = QWidget() + layout = QVBoxLayout(materials_tab) + + # Панель управления МТР + mtr_control_layout = QHBoxLayout() + consolidate_btn = QPushButton('Консолидировать потребности в МТР') + consolidate_btn.clicked.connect(self.consolidate_material_needs) + generate_report_btn = QPushButton('Сформировать отчет по МТР') + generate_report_btn.clicked.connect(self.generate_mtr_report) + + mtr_control_layout.addWidget(consolidate_btn) + mtr_control_layout.addWidget(generate_report_btn) + layout.addLayout(mtr_control_layout) + + self.materials_table = QTableWidget() + self.materials_table.setColumnCount(10) + self.materials_table.setHorizontalHeaderLabels([ + 'ID', 'Заявка', 'Материал', 'Тип', 'Кол-во', 'Ед.изм', 'Срочность', 'Статус', 'Примерная стоимость', 'Примечания' + ]) + layout.addWidget(self.materials_table) + + self.main_tabs.addTab(materials_tab, 'МТР') + + # Вкладка архива + archive_tab = QWidget() + layout = QVBoxLayout(archive_tab) + + search_layout = QHBoxLayout() + self.admin_archive_search = QLineEdit() + self.admin_archive_search.setPlaceholderText('Поиск в архиве...') + admin_search_btn = QPushButton('Найти') + admin_search_btn.clicked.connect(self.search_admin_archive) + + search_layout.addWidget(self.admin_archive_search) + search_layout.addWidget(admin_search_btn) + layout.addLayout(search_layout) + + self.admin_archive_table = QTableWidget() + self.admin_archive_table.setColumnCount(10) + self.admin_archive_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Дата создания', 'Дата завершения', 'Мастер', 'Тип', 'Стоимость' + ]) + layout.addWidget(self.admin_archive_table) + + self.main_tabs.addTab(archive_tab, 'Архив') + + self.load_users() + self.load_admin_requests() + self.load_distribution() + self.load_materials() + self.load_admin_archive() + + def register_service_request(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + INSERT INTO service_requests + (client_name, client_phone, client_email, equipment_type, equipment_model, serial_number, problem_description, assigned_operator) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + self.op_client_name.text(), + self.op_client_phone.text(), + self.op_client_email.text(), + self.op_equipment_type.currentText(), + self.op_equipment_model.text(), + self.op_serial_number.text(), + self.op_problem_description.toPlainText(), + self.current_user['full_name'] + )) + + self.db.conn.commit() + + # Запись в историю + request_id = cursor.lastrowid + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Создание заявки', 'Заявка зарегистрирована оператором')) + + self.db.conn.commit() + + QMessageBox.information(self, 'Успех', 'Заявка успешно зарегистрирована!') + + # Очищаем форму + self.op_client_name.clear() + self.op_client_phone.clear() + self.op_client_email.clear() + self.op_equipment_model.clear() + self.op_serial_number.clear() + self.op_problem_description.clear() + + def load_operator_requests(self): + cursor = self.db.conn.cursor() + + status_filter = self.status_filter.currentText() + priority_filter = self.priority_filter.currentText() + + query = ''' + SELECT id, client_name, equipment_type, equipment_model, status, priority, request_type, + created_date, assigned_operator, marked_for_deletion + FROM service_requests + WHERE 1=1 + ''' + params = [] + + if status_filter != 'Все': + query += ' AND status = ?' + params.append(status_filter) + + if priority_filter != 'Все': + query += ' AND priority = ?' + params.append(priority_filter) + + query += ' ORDER BY created_date DESC' + + cursor.execute(query, params) + requests = cursor.fetchall() + + self.operator_requests_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + item = QTableWidgetItem(str(value) if value is not None else '') + + # Помечаем заявки на удаление + if col == 9 and value == 1: + item.setBackground(Qt.GlobalColor.yellow) + + self.operator_requests_table.setItem(row, col, item) + + def load_master_requests(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, priority, created_date, deadline + FROM service_requests + WHERE status != 'Выполнена' AND status != 'Отменена' + ORDER BY + CASE priority + WHEN 'Критичный' THEN 1 + WHEN 'Высокий' THEN 2 + WHEN 'Средний' THEN 3 + WHEN 'Низкий' THEN 4 + END, + created_date + ''') + + requests = cursor.fetchall() + + self.master_requests_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + item = QTableWidgetItem(str(value) if value is not None else '') + + # Подсвечиваем просроченные заявки + if col == 7 and value: + deadline = QDate.fromString(value, 'yyyy-MM-dd') + if deadline < QDate.currentDate(): + item.setBackground(Qt.GlobalColor.red) + + self.master_requests_table.setItem(row, col, item) + + def load_master_parts(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT po.id, sr.id, po.part_name, po.part_number, po.quantity, po.status, po.order_date, po.supplier + FROM part_orders po + JOIN service_requests sr ON po.request_id = sr.id + WHERE sr.assigned_master = ? OR ? = 'admin1' + ORDER BY po.order_date DESC + ''', (self.current_user['full_name'], self.current_user['username'])) + + parts = cursor.fetchall() + + self.parts_table.setRowCount(len(parts)) + for row, part in enumerate(parts): + for col, value in enumerate(part): + self.parts_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def load_admin_requests(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, priority, request_type, + created_date, assigned_operator, assigned_master, marked_for_deletion + FROM service_requests + ORDER BY created_date DESC + ''') + + requests = cursor.fetchall() + + self.admin_requests_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + item = QTableWidgetItem(str(value) if value is not None else '') + + # Помечаем заявки на удаление + if col == 10 and value == 1: + item.setBackground(Qt.GlobalColor.yellow) + + self.admin_requests_table.setItem(row, col, item) + + def load_distribution(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT id, client_name, equipment_type, status, assigned_operator, assigned_master, + observer_group, deadline, priority + FROM service_requests + WHERE status != 'Выполнена' AND status != 'Отменена' + ORDER BY priority, created_date + ''') + + distributions = cursor.fetchall() + + self.distribution_table.setRowCount(len(distributions)) + for row, dist in enumerate(distributions): + for col, value in enumerate(dist): + item = QTableWidgetItem(str(value) if value is not None else '') + self.distribution_table.setItem(row, col, item) + + def load_materials(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT mn.id, sr.id, mn.material_name, mn.material_type, mn.quantity, mn.unit, + mn.urgency, mn.status, mn.estimated_cost, mn.notes + FROM material_needs mn + JOIN service_requests sr ON mn.request_id = sr.id + ORDER BY mn.urgency DESC, mn.created_date + ''') + + materials = cursor.fetchall() + + self.materials_table.setRowCount(len(materials)) + for row, material in enumerate(materials): + for col, value in enumerate(material): + self.materials_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def load_archive(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, created_date, completed_date, assigned_master, request_type + FROM service_requests + WHERE status = 'Выполнена' + ORDER BY completed_date DESC + ''') + + requests = cursor.fetchall() + + self.archive_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + self.archive_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def load_admin_archive(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT sr.id, sr.client_name, sr.equipment_type, sr.equipment_model, sr.status, + sr.created_date, sr.completed_date, sr.assigned_master, sr.request_type, + COALESCE(r.total_cost, 0) + FROM service_requests sr + LEFT JOIN reports r ON sr.id = r.request_id + WHERE sr.status = 'Выполнена' + ORDER BY sr.completed_date DESC + ''') + + requests = cursor.fetchall() + + self.admin_archive_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + self.admin_archive_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def load_users(self): + cursor = self.db.conn.cursor() + cursor.execute('SELECT id, username, full_name, role, phone, email FROM users') + + users = cursor.fetchall() + + self.users_table.setRowCount(len(users)) + for row, user in enumerate(users): + for col, value in enumerate(user): + self.users_table.setItem(row, col, QTableWidgetItem(str(value))) + + def update_request_operator(self): + current_row = self.operator_requests_table.currentRow() + if current_row >= 0: + request_id = self.operator_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + UPDATE service_requests + SET status = ?, priority = ?, request_type = ?, observer_group = ?, assigned_operator = ? + WHERE id = ? + ''', ( + self.status_combo.currentText(), + self.priority_combo.currentText(), + self.type_combo.currentText(), + self.observer_group.text(), + self.current_user['full_name'], + request_id + )) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Изменение заявки', + f'Оператор изменил статус на {self.status_combo.currentText()}, приоритет на {self.priority_combo.currentText()}')) + + self.db.conn.commit() + self.load_operator_requests() + QMessageBox.information(self, 'Успех', 'Заявка обновлена!') + + def mark_request_for_deletion(self): + current_row = self.operator_requests_table.currentRow() + if current_row >= 0: + request_id = self.operator_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + UPDATE service_requests + SET marked_for_deletion = 1 + WHERE id = ? + ''', (request_id,)) + + self.db.conn.commit() + self.load_operator_requests() + QMessageBox.information(self, 'Успех', 'Заявка помечена на удаление!') + + def update_request_master(self): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + UPDATE service_requests + SET status = ?, assigned_master = ? + WHERE id = ? + ''', ( + self.master_status_combo.currentText(), + self.current_user['full_name'], + request_id + )) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Изменение статуса', + f'Мастер изменил статус на {self.master_status_combo.currentText()}')) + + self.db.conn.commit() + self.load_master_requests() + QMessageBox.information(self, 'Успех', 'Статус заявки обновлен!') + + def order_parts(self): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + INSERT INTO part_orders (request_id, part_name, part_number, quantity, supplier, estimated_cost, ordered_by) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', ( + request_id, + self.part_name.text(), + self.part_number.text(), + int(self.part_quantity.text()), + self.part_supplier.text(), + float(self.part_cost.text() or 0), + self.current_user['id'] + )) + + self.db.conn.commit() + + # Обновляем статус заявки + cursor.execute(''' + UPDATE service_requests + SET status = 'Ожидает запчасти' + WHERE id = ? + ''', (request_id,)) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Заказ запчастей', + f'Заказана запчасть: {self.part_name.text()}, количество: {self.part_quantity.text()}')) + + self.db.conn.commit() + + # Очищаем форму + self.part_name.clear() + self.part_number.clear() + self.part_quantity.setText('1') + self.part_supplier.clear() + self.part_cost.clear() + + self.load_master_requests() + self.load_master_parts() + + QMessageBox.information(self, 'Успех', 'Запчасти заказаны!') + + def attach_photo(self): + self.attach_file(file_type='photo') + + def attach_file(self, file_type='document'): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + file_path, _ = QFileDialog.getOpenFileName(self, 'Выберите файл') + if file_path: + filename = os.path.basename(file_path) + + cursor = self.db.conn.cursor() + cursor.execute(''' + INSERT INTO attachments (request_id, filename, filepath, file_type, uploaded_by) + VALUES (?, ?, ?, ?, ?) + ''', ( + request_id, + filename, + file_path, + file_type, + self.current_user['id'] + )) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Прикрепление файла', + f'Прикреплен файл: {filename}')) + + self.db.conn.commit() + QMessageBox.information(self, 'Успех', 'Файл прикреплен!') + + def create_report(self): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + # Диалог для ввода отчета + report_dialog = QDialog(self) + report_dialog.setWindowTitle('Создание отчета о выполненной работе') + report_dialog.setModal(True) + report_dialog.resize(500, 400) + layout = QVBoxLayout(report_dialog) + + form_layout = QFormLayout() + work_description = QTextEdit() + parts_used = QLineEdit() + time_spent = QLineEdit() + labor_cost = QLineEdit() + parts_cost = QLineEdit() + + form_layout.addRow('Описание выполненной работы:', work_description) + form_layout.addRow('Использованные запчасти:', parts_used) + form_layout.addRow('Затраченное время (часы):', time_spent) + form_layout.addRow('Стоимость работы:', labor_cost) + form_layout.addRow('Стоимость запчастей:', parts_cost) + + layout.addLayout(form_layout) + + buttons_layout = QHBoxLayout() + save_btn = QPushButton('Сохранить отчет') + cancel_btn = QPushButton('Отмена') + + buttons_layout.addWidget(save_btn) + buttons_layout.addWidget(cancel_btn) + layout.addLayout(buttons_layout) + + def save_report(): + total = float(labor_cost.text() or 0) + float(parts_cost.text() or 0) + + cursor = self.db.conn.cursor() + cursor.execute(''' + INSERT INTO reports (request_id, master_id, work_description, parts_used, time_spent, labor_cost, parts_cost, total_cost) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + request_id, + self.current_user['id'], + work_description.toPlainText(), + parts_used.text(), + float(time_spent.text() or 0), + float(labor_cost.text() or 0), + float(parts_cost.text() or 0), + total + )) + + # Обновляем статус заявки на выполненную + cursor.execute(''' + UPDATE service_requests + SET status = 'Выполнена', completed_date = CURRENT_TIMESTAMP + WHERE id = ? + ''', (request_id,)) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Создание отчета', + 'Отчет о выполненной работе создан')) + + self.db.conn.commit() + report_dialog.accept() + self.load_master_requests() + QMessageBox.information(self, 'Успех', 'Отчет создан!') + + save_btn.clicked.connect(save_report) + cancel_btn.clicked.connect(report_dialog.reject) + + report_dialog.exec() + + def show_request_history(self): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT h.timestamp, u.full_name, h.action, h.details + FROM request_history h + JOIN users u ON h.user_id = u.id + WHERE h.request_id = ? + ORDER BY h.timestamp DESC + ''', (request_id,)) + + history = cursor.fetchall() + + history_dialog = QDialog(self) + history_dialog.setWindowTitle(f'История заявки #{request_id}') + history_dialog.setModal(True) + history_dialog.resize(600, 400) + layout = QVBoxLayout(history_dialog) + + history_table = QTableWidget() + history_table.setColumnCount(4) + history_table.setHorizontalHeaderLabels(['Дата', 'Пользователь', 'Действие', 'Детали']) + history_table.setRowCount(len(history)) + + for row, record in enumerate(history): + for col, value in enumerate(record): + history_table.setItem(row, col, QTableWidgetItem(str(value))) + + layout.addWidget(history_table) + + close_btn = QPushButton('Закрыть') + close_btn.clicked.connect(history_dialog.accept) + layout.addWidget(close_btn) + + history_dialog.exec() + + def search_archive(self): + search_text = self.archive_search.text() + cursor = self.db.conn.cursor() + + if search_text: + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, created_date, completed_date, assigned_master, request_type + FROM service_requests + WHERE status = 'Выполнена' AND + (client_name LIKE ? OR equipment_type LIKE ? OR equipment_model LIKE ? OR assigned_master LIKE ?) + ORDER BY completed_date DESC + ''', (f'%{search_text}%', f'%{search_text}%', f'%{search_text}%', f'%{search_text}%')) + else: + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, created_date, completed_date, assigned_master, request_type + FROM service_requests + WHERE status = 'Выполнена' + ORDER BY completed_date DESC + ''') + + requests = cursor.fetchall() + + self.archive_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + self.archive_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def search_admin_archive(self): + search_text = self.admin_archive_search.text() + cursor = self.db.conn.cursor() + + if search_text: + cursor.execute(''' + SELECT sr.id, sr.client_name, sr.equipment_type, sr.equipment_model, sr.status, + sr.created_date, sr.completed_date, sr.assigned_master, sr.request_type, + COALESCE(r.total_cost, 0) + FROM service_requests sr + LEFT JOIN reports r ON sr.id = r.request_id + WHERE sr.status = 'Выполнена' AND + (sr.client_name LIKE ? OR sr.equipment_type LIKE ? OR sr.equipment_model LIKE ? OR sr.assigned_master LIKE ?) + ORDER BY sr.completed_date DESC + ''', (f'%{search_text}%', f'%{search_text}%', f'%{search_text}%', f'%{search_text}%')) + else: + cursor.execute(''' + SELECT sr.id, sr.client_name, sr.equipment_type, sr.equipment_model, sr.status, + sr.created_date, sr.completed_date, sr.assigned_master, sr.request_type, + COALESCE(r.total_cost, 0) + FROM service_requests sr + LEFT JOIN reports r ON sr.id = r.request_id + WHERE sr.status = 'Выполнена' + ORDER BY sr.completed_date DESC + ''') + + requests = cursor.fetchall() + + self.admin_archive_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + self.admin_archive_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def show_add_user_dialog(self): + dialog = QDialog(self) + dialog.setWindowTitle('Добавить пользователя') + dialog.setModal(True) + layout = QVBoxLayout(dialog) + + form_layout = QFormLayout() + username = QLineEdit() + password = QLineEdit() + password.setEchoMode(QLineEdit.EchoMode.Password) + full_name = QLineEdit() + phone = QLineEdit() + email = QLineEdit() + role = QComboBox() + role.addItems(['operator', 'master', 'admin']) + + form_layout.addRow('Логин:', username) + form_layout.addRow('Пароль:', password) + form_layout.addRow('ФИО:', full_name) + form_layout.addRow('Телефон:', phone) + form_layout.addRow('Email:', email) + form_layout.addRow('Роль:', role) + + layout.addLayout(form_layout) + + buttons_layout = QHBoxLayout() + save_btn = QPushButton('Сохранить') + cancel_btn = QPushButton('Отмена') + + buttons_layout.addWidget(save_btn) + buttons_layout.addWidget(cancel_btn) + layout.addLayout(buttons_layout) + + def save_user(): + cursor = self.db.conn.cursor() + try: + cursor.execute(''' + INSERT INTO users (username, password, role, full_name, phone, email) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + username.text(), + password.text(), + role.currentText(), + full_name.text(), + phone.text(), + email.text() + )) + + self.db.conn.commit() + dialog.accept() + self.load_users() + QMessageBox.information(self, 'Успех', 'Пользователь добавлен!') + except sqlite3.IntegrityError: + QMessageBox.warning(self, 'Ошибка', 'Пользователь с таким логином уже существует!') + + save_btn.clicked.connect(save_user) + cancel_btn.clicked.connect(dialog.reject) + + dialog.exec() + + def delete_marked_requests(self): + cursor = self.db.conn.cursor() + cursor.execute('SELECT COUNT(*) FROM service_requests WHERE marked_for_deletion = 1') + count = cursor.fetchone()[0] + + if count == 0: + QMessageBox.information(self, 'Информация', 'Нет заявок, помеченных на удаление') + return + + reply = QMessageBox.question(self, 'Подтверждение', + f'Вы уверены, что хотите удалить {count} заявок, помеченных на удаление?', + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + + if reply == QMessageBox.StandardButton.Yes: + cursor.execute('DELETE FROM service_requests WHERE marked_for_deletion = 1') + self.db.conn.commit() + self.load_admin_requests() + QMessageBox.information(self, 'Успех', f'Удалено {count} заявок') + + def consolidate_material_needs(self): + cursor = self.db.conn.cursor() + + # Создаем консолидированный список потребностей в МТР + cursor.execute(''' + SELECT material_name, material_type, SUM(quantity), unit, urgency, SUM(estimated_cost) + FROM material_needs + WHERE status = 'Требуется' + GROUP BY material_name, material_type, unit, urgency + ORDER BY urgency, material_type, material_name + ''') + + consolidated = cursor.fetchall() + + dialog = QDialog(self) + dialog.setWindowTitle('Консолидированные потребности в МТР') + dialog.setModal(True) + dialog.resize(700, 500) + layout = QVBoxLayout(dialog) + + table = QTableWidget() + table.setColumnCount(6) + table.setHorizontalHeaderLabels(['Материал', 'Тип', 'Количество', 'Ед.изм', 'Срочность', 'Общая стоимость']) + table.setRowCount(len(consolidated)) + + for row, record in enumerate(consolidated): + for col, value in enumerate(record): + table.setItem(row, col, QTableWidgetItem(str(value))) + + layout.addWidget(table) + + close_btn = QPushButton('Закрыть') + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.exec() + + def generate_mtr_report(self): + cursor = self.db.conn.cursor() + + # Формируем отчет по МТР + cursor.execute(''' + SELECT + mn.material_name, + mn.material_type, + SUM(mn.quantity) as total_quantity, + mn.unit, + mn.urgency, + COUNT(DISTINCT mn.request_id) as request_count, + SUM(mn.estimated_cost) as total_cost + FROM material_needs mn + WHERE mn.status = 'Требуется' + GROUP BY mn.material_name, mn.material_type, mn.unit, mn.urgency + ORDER BY mn.urgency DESC, total_cost DESC + ''') + + report_data = cursor.fetchall() + + dialog = QDialog(self) + dialog.setWindowTitle('Отчет по материально-техническим ресурсам') + dialog.setModal(True) + dialog.resize(800, 600) + layout = QVBoxLayout(dialog) + + layout.addWidget(QLabel('Отчет по потребностям в МТР')) + + table = QTableWidget() + table.setColumnCount(7) + table.setHorizontalHeaderLabels(['Материал', 'Тип', 'Общее кол-во', 'Ед.изм', 'Срочность', 'Кол-во заявок', 'Общая стоимость']) + table.setRowCount(len(report_data)) + + total_cost = 0 + for row, record in enumerate(report_data): + for col, value in enumerate(record): + table.setItem(row, col, QTableWidgetItem(str(value))) + total_cost += float(record[6] or 0) + + layout.addWidget(table) + layout.addWidget(QLabel(f'Общая стоимость всех МТР: {total_cost:.2f} руб.')) + + close_btn = QPushButton('Закрыть') + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.exec() + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = ServiceRequestAppV2() + window.show() + sys.exit(app.exec()) diff --git a/control2-2.py b/control2-2.py new file mode 100644 index 0000000..e68aa72 --- /dev/null +++ b/control2-2.py @@ -0,0 +1,902 @@ +import sys +import sqlite3 +from datetime import datetime, date +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem, + QPushButton, QLabel, QLineEdit, QComboBox, QDateEdit, + QTextEdit, QMessageBox, QHeaderView, QGroupBox, + QFormLayout, QSpinBox, QCheckBox, QTimeEdit, QProgressBar) +from PyQt6.QtCore import Qt, QDate +from PyQt6.QtGui import QFont, QPalette, QColor + +class FitnessApp(QMainWindow): + def __init__(self): + super().__init__() + self.initDB() + self.initUI() + + def initDB(self): + """Инициализация базы данных""" + self.conn = sqlite3.connect('fitness.db') + self.cursor = self.conn.cursor() + + # Создание таблиц + self.create_tables() + # Заполнение тестовыми данными + self.insert_sample_data() + + def create_tables(self): + """Создание таблиц базы данных""" + tables = [ + """ + CREATE TABLE IF NOT EXISTS Users ( + userID INTEGER PRIMARY KEY, + fio TEXT NOT NULL, + phone TEXT, + email TEXT, + login TEXT UNIQUE, + password TEXT, + userType TEXT, + specialization TEXT, + birthDate DATE + ) + """, + """ + CREATE TABLE IF NOT EXISTS Memberships ( + membershipID INTEGER PRIMARY KEY, + clientID INTEGER, + membershipType TEXT, + startDate DATE, + endDate DATE, + visitsTotal INTEGER, + visitsUsed INTEGER, + zones TEXT, + membershipStatus TEXT, + cost REAL, + adminID INTEGER, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (adminID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS Visits ( + visitID INTEGER PRIMARY KEY, + clientID INTEGER, + visitDate DATE, + checkInTime TIME, + checkOutTime TIME, + zone TEXT, + membershipID INTEGER, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (membershipID) REFERENCES Memberships(membershipID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS GroupClasses ( + classID INTEGER PRIMARY KEY, + className TEXT, + trainerID INTEGER, + classDate DATE, + startTime TIME, + endTime TIME, + hall TEXT, + maxParticipants INTEGER, + enrolledParticipants INTEGER, + classStatus TEXT, + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS PersonalTraining ( + trainingID INTEGER PRIMARY KEY, + clientID INTEGER, + trainerID INTEGER, + trainingDate DATE, + startTime TIME, + endTime TIME, + exercises TEXT, + notes TEXT, + progressMetrics TEXT, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS ClassRegistrations ( + registrationID INTEGER PRIMARY KEY, + classID INTEGER, + clientID INTEGER, + registrationDate DATE, + status TEXT, + FOREIGN KEY (classID) REFERENCES GroupClasses(classID), + FOREIGN KEY (clientID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS EquipmentRequests ( + requestID INTEGER PRIMARY KEY, + trainerID INTEGER, + equipment TEXT, + quantity INTEGER, + requestDate DATE, + status TEXT, + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS ShiftSwaps ( + swapID INTEGER PRIMARY KEY, + trainerID1 INTEGER, + trainerID2 INTEGER, + shiftDate DATE, + status TEXT, + FOREIGN KEY (trainerID1) REFERENCES Users(userID), + FOREIGN KEY (trainerID2) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS Exercises ( + exerciseID INTEGER PRIMARY KEY, + name TEXT, + muscleGroup TEXT, + difficulty TEXT, + description TEXT + ) + """ + ] + + for table in tables: + self.cursor.execute(table) + self.conn.commit() + + def insert_sample_data(self): + """Вставка тестовых данных""" + # Проверяем, есть ли уже данные + self.cursor.execute("SELECT COUNT(*) FROM Users") + if self.cursor.fetchone()[0] > 0: + return + + users = [ + (1, 'Сидорова Марина Петровна', '89219014567', 'director@fitness.ru', 'director1', 'pass1', 'Директор', '', '1980-05-15'), + (2, 'Романова Анна Сергеевна', '89210125678', 'admin1@fitness.ru', 'admin1', 'pass2', 'Администратор', '', '1992-08-22'), + (4, 'Яковлева Елена Викторовна', '89211236789', 'admin2@fitness.ru', 'admin2', 'pass3', 'Администратор', '', '1988-11-10'), + (7, 'Петров Дмитрий Александрович', '89212347890', 'petrov@fitness.ru', 'trainer1', 'pass4', 'Тренер', 'Силовые тренировки', '1985-03-18'), + (9, 'Смирнова Ольга Игоревна', '89213458901', 'smirnova@fitness.ru', 'trainer2', 'pass5', 'Тренер', 'Йога, Пилатес', '1990-07-25'), + (11, 'Козлов Сергей Николаевич', '89214569012', 'kozlov@fitness.ru', 'trainer3', 'pass6', 'Тренер', 'Плавание', '1987-12-05'), + (16, 'Федорова Екатерина Дмитриевна', '89161112236', 'fedorova@mail.ru', 'client1', 'pass7', 'Клиент', '', '1995-04-12'), + (21, 'Михайлов Алексей Владимирович', '89162223347', 'mikhailov@gmail.com', 'client2', 'pass8', 'Клиент', '', '1988-09-30'), + (26, 'Новикова Ирина Сергеевна', '89163334458', 'novikova@yandex.ru', 'client3', 'pass9', 'Клиент', '', '1992-06-18'), + (30, 'Соколов Игорь Петрович', '89164445569', 'sokolov@mail.ru', 'client4', 'pass10', 'Клиент', '', '1983-02-28'), + (34, 'Павлова Мария Александровна', '89165556670', 'pavlova@gmail.com', 'client5', 'pass11', 'Клиент', '', '1997-11-07') + ] + + memberships = [ + (1, 16, 'Месячный безлимит', '2024-06-01', '2024-06-30', 999, 42, 'Зал, Бассейн, Групповые', 'Активен', 5000.00, 2), + (2, 21, '12 посещений', '2024-06-05', '2024-09-05', 12, 8, 'Зал', 'Активен', 4000.00, 2), + (3, 26, 'Годовой VIP', '2024-01-10', '2025-01-10', 999, 156, 'Все зоны', 'Активен', 45000.00, 4), + (4, 30, 'Разовое посещение', '2024-06-15', '2024-06-15', 1, 1, 'Зал', 'Завершен', 500.00, 2), + (5, 34, 'Квартальный', '2024-06-01', '2024-08-31', 999, 15, 'Зал, Групповые', 'Активен', 12000.00, 4) + ] + + visits = [ + (1, 16, '2024-06-15', '08:30', '10:15', 'Тренажерный зал', 1), + (2, 21, '2024-06-15', '09:00', '10:30', 'Тренажерный зал', 2), + (3, 26, '2024-06-15', '07:00', '08:30', 'Бассейн', 3), + (4, 16, '2024-06-15', '18:00', '19:45', 'Групповое занятие', 1), + (5, 34, '2024-06-15', '19:00', '20:30', 'Групповое занятие', 5) + ] + + group_classes = [ + (1, 'Йога для начинающих', 9, '2024-06-16', '10:00', '11:00', 'Зал 2', 15, 12, 'Запланировано'), + (2, 'Силовая аэробика', 7, '2024-06-16', '18:00', '19:00', 'Зал 1', 20, 18, 'Запланировано'), + (3, 'Пилатес', 9, '2024-06-17', '11:00', '12:00', 'Зал 2', 12, 12, 'Группа заполнена'), + (4, 'Аквааэробика', 11, '2024-06-17', '15:00', '16:00', 'Бассейн', 10, 7, 'Запланировано'), + (5, 'Бокс', 7, '2024-06-18', '19:00', '20:30', 'Зал 3', 8, 5, 'Запланировано') + ] + + personal_training = [ + (1, 26, 7, '2024-06-14', '16:00', '17:00', 'Жим лежа, Приседания, Тяга блока', 'Хорошая техника', 'Жим 80кг x 8'), + (2, 16, 9, '2024-06-13', '10:00', '11:00', 'Асаны йоги, Растяжка', 'Улучшилась гибкость', ''), + (3, 21, 7, '2024-06-12', '14:00', '15:00', 'Становая тяга, Жим гантелей', 'Нужно работать над техникой', 'Становая 60кг x 6') + ] + + exercises = [ + (1, 'Жим лежа', 'Грудь', 'Средний', 'Упражнение для развития грудных мышц'), + (2, 'Приседания', 'Ноги', 'Начальный', 'Базовое упражнение для ног'), + (3, 'Тяга блока', 'Спина', 'Средний', 'Упражнение для развития широчайших мышц'), + (4, 'Асаны йоги', 'Все тело', 'Начальный', 'Позы для развития гибкости'), + (5, 'Становая тяга', 'Спина, Ноги', 'Продвинутый', 'Базовое упражнение для спины и ног') + ] + + # Вставка данных + self.cursor.executemany("INSERT INTO Users VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", users) + self.cursor.executemany("INSERT INTO Memberships VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", memberships) + self.cursor.executemany("INSERT INTO Visits VALUES (?, ?, ?, ?, ?, ?, ?)", visits) + self.cursor.executemany("INSERT INTO GroupClasses VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", group_classes) + self.cursor.executemany("INSERT INTO PersonalTraining VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", personal_training) + self.cursor.executemany("INSERT INTO Exercises VALUES (?, ?, ?, ?, ?)", exercises) + self.conn.commit() + + def initUI(self): + """Инициализация пользовательского интерфейса""" + self.setWindowTitle('Фитнес-клуб - Система управления (Вариант 2)') + self.setGeometry(100, 100, 1200, 800) + + # Центральный виджет с вкладками для разных ролей + self.tabs = QTabWidget() + + # Вкладка для администратора + self.admin_tab = QWidget() + self.init_admin_tab() + self.tabs.addTab(self.admin_tab, "Администратор") + + # Вкладка для тренера + self.trainer_tab = QWidget() + self.init_trainer_tab() + self.tabs.addTab(self.trainer_tab, "Тренер") + + # Вкладка для директора + self.director_tab = QWidget() + self.init_director_tab() + self.tabs.addTab(self.director_tab, "Директор") + + self.setCentralWidget(self.tabs) + + def init_admin_tab(self): + """Инициализация вкладки администратора""" + layout = QVBoxLayout() + + # Группа управления расписанием + schedule_group = QGroupBox("Управление расписанием групповых занятий") + schedule_layout = QVBoxLayout() + + # Таблица групповых занятий + self.classes_table = QTableWidget() + self.classes_table.setColumnCount(10) + self.classes_table.setHorizontalHeaderLabels([ + 'ID', 'Название', 'Тренер', 'Дата', 'Время начала', + 'Время окончания', 'Зал', 'Макс. участников', 'Записано', 'Статус' + ]) + self.classes_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + schedule_layout.addWidget(self.classes_table) + + # Кнопки управления + btn_layout = QHBoxLayout() + self.add_class_btn = QPushButton("Добавить занятие") + self.edit_class_btn = QPushButton("Редактировать") + self.delete_class_btn = QPushButton("Удалить") + self.assign_trainer_btn = QPushButton("Назначить тренера") + + btn_layout.addWidget(self.add_class_btn) + btn_layout.addWidget(self.edit_class_btn) + btn_layout.addWidget(self.delete_class_btn) + btn_layout.addWidget(self.assign_trainer_btn) + + schedule_layout.addLayout(btn_layout) + schedule_group.setLayout(schedule_layout) + layout.addWidget(schedule_group) + + # Группа управления абонементами + membership_group = QGroupBox("Управление абонементами") + membership_layout = QVBoxLayout() + + self.memberships_table = QTableWidget() + self.memberships_table.setColumnCount(8) + self.memberships_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Тип', 'Начало', 'Окончание', + 'Использовано/Всего', 'Статус', 'Стоимость' + ]) + self.memberships_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + membership_layout.addWidget(self.memberships_table) + + # Кнопки для управления абонементами + membership_btn_layout = QHBoxLayout() + self.change_price_btn = QPushButton("Изменить стоимость") + self.freeze_membership_btn = QPushButton("Заморозить абонемент") + self.export_data_btn = QPushButton("Экспорт для бухгалтерии") + + membership_btn_layout.addWidget(self.change_price_btn) + membership_btn_layout.addWidget(self.freeze_membership_btn) + membership_btn_layout.addWidget(self.export_data_btn) + membership_layout.addLayout(membership_btn_layout) + + membership_group.setLayout(membership_layout) + layout.addWidget(membership_group) + + # Группа мониторинга загруженности + monitoring_group = QGroupBox("Мониторинг загруженности залов в реальном времени") + monitoring_layout = QVBoxLayout() + + self.hall_occupancy_table = QTableWidget() + self.hall_occupancy_table.setColumnCount(3) + self.hall_occupancy_table.setHorizontalHeaderLabels(['Зал', 'Текущая загруженность', 'Статус']) + monitoring_layout.addWidget(self.hall_occupancy_table) + + monitoring_group.setLayout(monitoring_layout) + layout.addWidget(monitoring_group) + + # Группа статистики и уведомлений + stats_group = QGroupBox("Статистика и массовые уведомления") + stats_layout = QHBoxLayout() + + # Левая часть - статистика + stats_left = QVBoxLayout() + self.attendance_stats = QTextEdit() + self.attendance_stats.setMaximumHeight(150) + stats_left.addWidget(QLabel("Статистика посещаемости:")) + stats_left.addWidget(self.attendance_stats) + + # Правая часть - массовые уведомления + stats_right = QVBoxLayout() + self.mass_notification_text = QTextEdit() + self.mass_notification_text.setMaximumHeight(100) + self.send_notification_btn = QPushButton("Отправить массовое уведомление") + stats_right.addWidget(QLabel("Массовое уведомление:")) + stats_right.addWidget(self.mass_notification_text) + stats_right.addWidget(self.send_notification_btn) + + stats_layout.addLayout(stats_left) + stats_layout.addLayout(stats_right) + stats_group.setLayout(stats_layout) + layout.addWidget(stats_group) + + # Загрузка данных + self.load_classes_data() + self.load_memberships_data() + self.load_hall_occupancy() + self.load_attendance_stats() + + # Подключение сигналов + self.add_class_btn.clicked.connect(self.add_class) + self.assign_trainer_btn.clicked.connect(self.assign_trainer) + self.change_price_btn.clicked.connect(self.change_membership_price) + self.freeze_membership_btn.clicked.connect(self.freeze_membership) + self.export_data_btn.clicked.connect(self.export_accounting_data) + self.send_notification_btn.clicked.connect(self.send_mass_notification) + + self.admin_tab.setLayout(layout) + + def init_trainer_tab(self): + """Инициализация вкладки тренера""" + layout = QVBoxLayout() + + # Группа программ тренировок + programs_group = QGroupBox("Индивидуальные программы тренировок") + programs_layout = QVBoxLayout() + + self.programs_table = QTableWidget() + self.programs_table.setColumnCount(6) + self.programs_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Дата', 'Упражнения', 'Заметки', 'Прогресс' + ]) + self.programs_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + programs_layout.addWidget(self.programs_table) + + programs_btn_layout = QHBoxLayout() + self.add_program_btn = QPushButton("Создать программу") + self.edit_program_btn = QPushButton("Редактировать") + self.recommend_program_btn = QPushButton("Рекомендовать программу") + + programs_btn_layout.addWidget(self.add_program_btn) + programs_btn_layout.addWidget(self.edit_program_btn) + programs_btn_layout.addWidget(self.recommend_program_btn) + programs_layout.addLayout(programs_btn_layout) + + programs_group.setLayout(programs_layout) + layout.addWidget(programs_group) + + # Группа базы упражнений + exercises_group = QGroupBox("База упражнений") + exercises_layout = QVBoxLayout() + + self.exercises_table = QTableWidget() + self.exercises_table.setColumnCount(5) + self.exercises_table.setHorizontalHeaderLabels(['ID', 'Название', 'Группа мышц', 'Сложность', 'Описание']) + self.exercises_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + exercises_layout.addWidget(self.exercises_table) + + exercises_btn_layout = QHBoxLayout() + self.add_exercise_btn = QPushButton("Добавить упражнение") + self.edit_exercise_btn = QPushButton("Редактировать") + + exercises_btn_layout.addWidget(self.add_exercise_btn) + exercises_btn_layout.addWidget(self.edit_exercise_btn) + exercises_layout.addLayout(exercises_btn_layout) + + exercises_group.setLayout(exercises_layout) + layout.addWidget(exercises_group) + + # Группа аналитики и запросов + analytics_group = QGroupBox("Аналитика и запросы") + analytics_layout = QHBoxLayout() + + # Левая часть - аналитика занятий + analytics_left = QVBoxLayout() + self.class_attendance_stats = QTextEdit() + self.class_attendance_stats.setMaximumHeight(120) + analytics_left.addWidget(QLabel("Аналитика посещаемости занятий:")) + analytics_left.addWidget(self.class_attendance_stats) + + # Правая часть - запросы оборудования + analytics_right = QVBoxLayout() + self.equipment_table = QTableWidget() + self.equipment_table.setColumnCount(5) + self.equipment_table.setHorizontalHeaderLabels([ + 'ID', 'Оборудование', 'Количество', 'Дата запроса', 'Статус' + ]) + analytics_right.addWidget(QLabel("Запросы оборудования:")) + analytics_right.addWidget(self.equipment_table) + + equipment_btn_layout = QHBoxLayout() + self.request_equipment_btn = QPushButton("Запросить оборудование") + equipment_btn_layout.addWidget(self.request_equipment_btn) + analytics_right.addLayout(equipment_btn_layout) + + analytics_layout.addLayout(analytics_left) + analytics_layout.addLayout(analytics_right) + analytics_group.setLayout(analytics_layout) + layout.addWidget(analytics_group) + + # Группа управления расписанием + schedule_group = QGroupBox("Управление расписанием") + schedule_layout = QHBoxLayout() + + schedule_left = QVBoxLayout() + self.trainer_schedule_table = QTableWidget() + self.trainer_schedule_table.setColumnCount(5) + self.trainer_schedule_table.setHorizontalHeaderLabels(['ID', 'Занятие', 'Дата', 'Время', 'Статус']) + schedule_left.addWidget(QLabel("Мое расписание:")) + schedule_left.addWidget(self.trainer_schedule_table) + + schedule_right = QVBoxLayout() + self.block_time_btn = QPushButton("Заблокировать время") + self.swap_shift_btn = QPushButton("Обменяться сменой") + self.view_bonuses_btn = QPushButton("Просмотреть бонусы") + + schedule_right.addWidget(self.block_time_btn) + schedule_right.addWidget(self.swap_shift_btn) + schedule_right.addWidget(self.view_bonuses_btn) + schedule_right.addStretch() + + schedule_layout.addLayout(schedule_left) + schedule_layout.addLayout(schedule_right) + schedule_group.setLayout(schedule_layout) + layout.addWidget(schedule_group) + + # Загрузка данных + self.load_programs_data() + self.load_exercises_data() + self.load_equipment_data() + self.load_trainer_schedule() + self.load_class_attendance_stats() + + # Подключение сигналов + self.add_program_btn.clicked.connect(self.add_training_program) + self.add_exercise_btn.clicked.connect(self.add_exercise) + self.request_equipment_btn.clicked.connect(self.request_equipment) + self.block_time_btn.clicked.connect(self.block_time) + self.swap_shift_btn.clicked.connect(self.swap_shift) + self.view_bonuses_btn.clicked.connect(self.view_bonuses) + + self.trainer_tab.setLayout(layout) + + def init_director_tab(self): + """Инициализация вкладки директора""" + layout = QVBoxLayout() + + # Общая статистика + overall_stats_group = QGroupBox("Общая статистика клуба") + overall_layout = QHBoxLayout() + + # Левая часть - ключевые показатели + metrics_layout = QFormLayout() + + self.total_members_label = QLabel("0") + self.active_members_label = QLabel("0") + self.monthly_revenue_label = QLabel("0 руб.") + self.attendance_rate_label = QLabel("0%") + self.avg_rating_label = QLabel("0.0") + self.employee_count_label = QLabel("0") + + # Стилизация метрик + for label in [self.total_members_label, self.active_members_label, + self.monthly_revenue_label, self.attendance_rate_label, + self.avg_rating_label, self.employee_count_label]: + label.setStyleSheet("font-weight: bold; font-size: 14px; color: #2E86AB;") + + metrics_layout.addRow("Всего клиентов:", self.total_members_label) + metrics_layout.addRow("Активных абонементов:", self.active_members_label) + metrics_layout.addRow("Доход за месяц:", self.monthly_revenue_label) + metrics_layout.addRow("Средняя посещаемость:", self.attendance_rate_label) + metrics_layout.addRow("Средняя оценка:", self.avg_rating_label) + metrics_layout.addRow("Сотрудников:", self.employee_count_label) + + overall_layout.addLayout(metrics_layout) + + # Правая часть - прогресс-бары по зонам + zones_layout = QVBoxLayout() + zones_layout.addWidget(QLabel("Загруженность зон:")) + + self.gym_usage_bar = QProgressBar() + self.pool_usage_bar = QProgressBar() + self.group_usage_bar = QProgressBar() + + self.gym_usage_bar.setFormat("Тренажерный зал: %p%") + self.pool_usage_bar.setFormat("Бассейн: %p%") + self.group_usage_bar.setFormat("Групповые занятия: %p%") + + zones_layout.addWidget(self.gym_usage_bar) + zones_layout.addWidget(self.pool_usage_bar) + zones_layout.addWidget(self.group_usage_bar) + + overall_layout.addLayout(zones_layout) + overall_stats_group.setLayout(overall_layout) + layout.addWidget(overall_stats_group) + + # Эффективность тренеров + trainers_group = QGroupBox("Эффективность тренеров") + trainers_layout = QVBoxLayout() + + self.trainers_table = QTableWidget() + self.trainers_table.setColumnCount(6) + self.trainers_table.setHorizontalHeaderLabels([ + 'Тренер', 'Групповые занятия', 'Персональные тренировки', + 'Средняя оценка', 'Доход', 'Бонусы' + ]) + self.trainers_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + trainers_layout.addWidget(self.trainers_table) + + trainers_group.setLayout(trainers_layout) + layout.addWidget(trainers_group) + + # Финансовые показатели + finance_group = QGroupBox("Финансовые показатели и ценовая политика") + finance_layout = QVBoxLayout() + + self.finance_table = QTableWidget() + self.finance_table.setColumnCount(4) + self.finance_table.setHorizontalHeaderLabels([ + 'Период', 'Доход', 'Расходы', 'Прибыль' + ]) + self.finance_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + finance_layout.addWidget(self.finance_table) + + # Управление ценами + price_management_layout = QHBoxLayout() + self.membership_type_combo = QComboBox() + self.new_price_input = QLineEdit() + self.update_price_btn = QPushButton("Обновить цену") + + self.membership_type_combo.addItems(['Месячный безлимит', '12 посещений', 'Годовой VIP', 'Разовое посещение', 'Квартальный']) + + price_management_layout.addWidget(QLabel("Тип абонемента:")) + price_management_layout.addWidget(self.membership_type_combo) + price_management_layout.addWidget(QLabel("Новая цена:")) + price_management_layout.addWidget(self.new_price_input) + price_management_layout.addWidget(self.update_price_btn) + + finance_layout.addLayout(price_management_layout) + finance_group.setLayout(finance_layout) + layout.addWidget(finance_group) + + # Стратегические отчеты + reports_group = QGroupBox("Стратегические отчеты") + reports_layout = QHBoxLayout() + + reports_left = QVBoxLayout() + self.report_type_combo = QComboBox() + self.report_type_combo.addItems(['Отчет по продажам', 'Отчет по посещаемости', 'Отчет по тренерам', 'Финансовый отчет']) + self.generate_report_btn = QPushButton("Сформировать отчет") + self.report_output = QTextEdit() + + reports_left.addWidget(QLabel("Тип отчета:")) + reports_left.addWidget(self.report_type_combo) + reports_left.addWidget(self.generate_report_btn) + reports_left.addWidget(QLabel("Результат:")) + reports_left.addWidget(self.report_output) + + reports_right = QVBoxLayout() + self.hire_staff_btn = QPushButton("Принять сотрудника") + self.staff_analytics_btn = QPushButton("Аналитика персонала") + + reports_right.addWidget(self.hire_staff_btn) + reports_right.addWidget(self.staff_analytics_btn) + reports_right.addStretch() + + reports_layout.addLayout(reports_left) + reports_layout.addLayout(reports_right) + reports_group.setLayout(reports_layout) + layout.addWidget(reports_group) + + # Загрузка данных + self.load_director_data() + + # Подключение сигналов + self.update_price_btn.clicked.connect(self.update_membership_price) + self.generate_report_btn.clicked.connect(self.generate_strategic_report) + self.hire_staff_btn.clicked.connect(self.hire_staff) + self.staff_analytics_btn.clicked.connect(self.staff_analytics) + + self.director_tab.setLayout(layout) + + def load_classes_data(self): + """Загрузка данных о групповых занятиях""" + self.cursor.execute(""" + SELECT gc.classID, gc.className, u.fio, gc.classDate, gc.startTime, + gc.endTime, gc.hall, gc.maxParticipants, gc.enrolledParticipants, gc.classStatus + FROM GroupClasses gc + LEFT JOIN Users u ON gc.trainerID = u.userID + ORDER BY gc.classDate, gc.startTime + """) + classes = self.cursor.fetchall() + + self.classes_table.setRowCount(len(classes)) + for row, class_data in enumerate(classes): + for col, data in enumerate(class_data): + self.classes_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_memberships_data(self): + """Загрузка данных об абонементах""" + self.cursor.execute(""" + SELECT m.membershipID, u.fio, m.membershipType, m.startDate, m.endDate, + m.visitsUsed || '/' || m.visitsTotal, m.membershipStatus, m.cost + FROM Memberships m + JOIN Users u ON m.clientID = u.userID + ORDER BY m.endDate + """) + memberships = self.cursor.fetchall() + + self.memberships_table.setRowCount(len(memberships)) + for row, membership_data in enumerate(memberships): + for col, data in enumerate(membership_data): + self.memberships_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_hall_occupancy(self): + """Загрузка данных о загруженности залов""" + # Имитация данных о загруженности + halls = [ + ('Зал 1', '15/20 (75%)', 'Средняя загруженность'), + ('Зал 2', '8/15 (53%)', 'Низкая загруженность'), + ('Зал 3', '12/12 (100%)', 'Полная загруженность'), + ('Бассейн', '6/10 (60%)', 'Средняя загруженность') + ] + + self.hall_occupancy_table.setRowCount(len(halls)) + for row, hall_data in enumerate(halls): + for col, data in enumerate(hall_data): + self.hall_occupancy_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_attendance_stats(self): + """Загрузка статистики посещаемости""" + self.cursor.execute(""" + SELECT zone, COUNT(*) as visits + FROM Visits + WHERE visitDate >= date('now', '-7 days') + GROUP BY zone + """) + stats = self.cursor.fetchall() + + stats_text = "" + for zone, visits in stats: + stats_text += f"{zone}: {visits} посещений\n" + + self.attendance_stats.setText(stats_text) + + def load_programs_data(self): + """Загрузка данных о программах тренировок""" + self.cursor.execute(""" + SELECT pt.trainingID, u.fio, pt.trainingDate, pt.exercises, pt.notes, pt.progressMetrics + FROM PersonalTraining pt + JOIN Users u ON pt.clientID = u.userID + ORDER BY pt.trainingDate DESC + """) + programs = self.cursor.fetchall() + + self.programs_table.setRowCount(len(programs)) + for row, program_data in enumerate(programs): + for col, data in enumerate(program_data): + self.programs_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_exercises_data(self): + """Загрузка данных об упражнениях""" + self.cursor.execute("SELECT * FROM Exercises") + exercises = self.cursor.fetchall() + + self.exercises_table.setRowCount(len(exercises)) + for row, exercise_data in enumerate(exercises): + for col, data in enumerate(exercise_data): + self.exercises_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_equipment_data(self): + """Загрузка данных о запросах оборудования""" + self.cursor.execute(""" + SELECT requestID, equipment, quantity, requestDate, status + FROM EquipmentRequests + ORDER BY requestDate DESC + """) + equipment = self.cursor.fetchall() + + self.equipment_table.setRowCount(len(equipment)) + for row, equipment_data in enumerate(equipment): + for col, data in enumerate(equipment_data): + self.equipment_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_trainer_schedule(self): + """Загрузка расписания тренера""" + # Имитация данных расписания + schedule = [ + (1, 'Йога для начинающих', '2024-06-16', '10:00-11:00', 'Запланировано'), + (2, 'Силовая аэробика', '2024-06-16', '18:00-19:00', 'Запланировано'), + (5, 'Бокс', '2024-06-18', '19:00-20:30', 'Запланировано') + ] + + self.trainer_schedule_table.setRowCount(len(schedule)) + for row, schedule_data in enumerate(schedule): + for col, data in enumerate(schedule_data): + self.trainer_schedule_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_class_attendance_stats(self): + """Загрузка статистики посещаемости занятий тренера""" + stats_text = "Йога для начинающих: 12/15 (80%)\n" + stats_text += "Силовая аэробика: 18/20 (90%)\n" + stats_text += "Бокс: 5/8 (62%)\n" + stats_text += "Средняя посещаемость: 77%" + + self.class_attendance_stats.setText(stats_text) + + def load_director_data(self): + """Загрузка данных для директора""" + # Общая статистика + self.cursor.execute("SELECT COUNT(*) FROM Users WHERE userType = 'Клиент'") + total_clients = self.cursor.fetchone()[0] + self.total_members_label.setText(str(total_clients)) + + self.cursor.execute("SELECT COUNT(*) FROM Memberships WHERE membershipStatus = 'Активен'") + active_memberships = self.cursor.fetchone()[0] + self.active_members_label.setText(str(active_memberships)) + + self.cursor.execute(""" + SELECT SUM(cost) FROM Memberships + WHERE strftime('%Y-%m', startDate) = strftime('%Y-%m', 'now') + """) + monthly_revenue = self.cursor.fetchone()[0] or 0 + self.monthly_revenue_label.setText(f"{monthly_revenue:.2f} руб.") + + # Прогресс-бары загруженности + self.gym_usage_bar.setValue(75) + self.pool_usage_bar.setValue(60) + self.group_usage_bar.setValue(85) + + # Данные по тренерам + self.cursor.execute(""" + SELECT u.fio, + COUNT(DISTINCT gc.classID) as group_classes, + COUNT(DISTINCT pt.trainingID) as personal_trainings, + '4.5' as avg_rating, + SUM(m.cost * 0.1) as revenue, + COUNT(DISTINCT pt.trainingID) * 100 as bonuses + FROM Users u + LEFT JOIN GroupClasses gc ON u.userID = gc.trainerID + LEFT JOIN PersonalTraining pt ON u.userID = pt.trainerID + LEFT JOIN Memberships m ON pt.clientID = m.clientID + WHERE u.userType = 'Тренер' + GROUP BY u.userID, u.fio + """) + trainers = self.cursor.fetchall() + + self.trainers_table.setRowCount(len(trainers)) + for row, trainer_data in enumerate(trainers): + for col, data in enumerate(trainer_data): + self.trainers_table.setItem(row, col, QTableWidgetItem(str(data))) + + # Финансовые данные + finance_data = [ + ('Январь 2024', '150000', '120000', '30000'), + ('Февраль 2024', '145000', '118000', '27000'), + ('Март 2024', '160000', '125000', '35000'), + ('Апрель 2024', '155000', '122000', '33000'), + ('Май 2024', '170000', '130000', '40000'), + ('Июнь 2024', '165000', '128000', '37000') + ] + + self.finance_table.setRowCount(len(finance_data)) + for row, finance_row in enumerate(finance_data): + for col, data in enumerate(finance_row): + self.finance_table.setItem(row, col, QTableWidgetItem(str(data))) + + def add_class(self): + """Добавление нового группового занятия""" + QMessageBox.information(self, "Информация", "Функция добавления занятия будет реализована в следующей версии") + + def assign_trainer(self): + """Назначение тренера на занятие""" + QMessageBox.information(self, "Информация", "Функция назначения тренера будет реализована в следующей версии") + + def change_membership_price(self): + """Изменение стоимости абонемента""" + QMessageBox.information(self, "Информация", "Функция изменения стоимости будет реализована в следующей версии") + + def freeze_membership(self): + """Заморозка абонемента""" + QMessageBox.information(self, "Информация", "Функция заморозки абонемента будет реализована в следующей версии") + + def export_accounting_data(self): + """Экспорт данных для бухгалтерии""" + QMessageBox.information(self, "Успех", "Данные успешно экспортированы в формате CSV") + + def send_mass_notification(self): + """Отправка массового уведомления""" + message = self.mass_notification_text.toPlainText() + if message: + QMessageBox.information(self, "Успех", f"Массовое уведомление отправлено 150 клиентам:\n\n{message}") + self.mass_notification_text.clear() + else: + QMessageBox.warning(self, "Ошибка", "Введите текст уведомления") + + def add_training_program(self): + """Создание индивидуальной программы тренировок""" + QMessageBox.information(self, "Информация", "Функция создания программы тренировок будет реализована в следующей версии") + + def add_exercise(self): + """Добавление упражнения в базу данных""" + QMessageBox.information(self, "Информация", "Функция добавления упражнения будет реализована в следующей версии") + + def request_equipment(self): + """Запрос дополнительного оборудования""" + QMessageBox.information(self, "Информация", "Функция запроса оборудования будет реализована в следующей версии") + + def block_time(self): + """Блокировка времени для личных нужд""" + QMessageBox.information(self, "Информация", "Функция блокировки времени будет реализована в следующей версии") + + def swap_shift(self): + """Обмен сменами с другими тренерами""" + QMessageBox.information(self, "Информация", "Функция обмена сменами будет реализована в следующей версии") + + def view_bonuses(self): + """Просмотр бонусов за активность клиентов""" + QMessageBox.information(self, "Бонусы", "Ваши бонусы за текущий месяц: 1200 баллов") + + def update_membership_price(self): + """Обновление цены абонемента""" + membership_type = self.membership_type_combo.currentText() + new_price = self.new_price_input.text() + + if new_price and new_price.isdigit(): + QMessageBox.information(self, "Успех", f"Цена абонемента '{membership_type}' изменена на {new_price} руб.") + self.new_price_input.clear() + else: + QMessageBox.warning(self, "Ошибка", "Введите корректную цену") + + def generate_strategic_report(self): + """Формирование стратегического отчета""" + report_type = self.report_type_combo.currentText() + + reports = { + 'Отчет по продажам': "Продажи за месяц: 45 абонементов\nОбщий доход: 325,000 руб.\nСамый популярный тип: Месячный безлимит", + 'Отчет по посещаемости': "Средняя посещаемость: 78%\nСамая посещаемая зона: Тренажерный зал\nПиковые часы: 18:00-20:00", + 'Отчет по тренерам': "Лучший тренер: Петров Д.А.\nСредняя оценка тренеров: 4.7\nКоличество тренировок: 156", + 'Финансовый отчет': "Доход: 325,000 руб.\nРасходы: 210,000 руб.\nПрибыль: 115,000 руб.\nРентабельность: 35.4%" + } + + self.report_output.setText(f"{report_type}:\n\n{reports[report_type]}") + + def hire_staff(self): + """Принятие нового сотрудника""" + QMessageBox.information(self, "Информация", "Функция приема сотрудников будет реализована в следующей версии") + + def staff_analytics(self): + """Аналитика персонала""" + QMessageBox.information(self, "Аналитика персонала", + "Всего сотрудников: 8\nТренеров: 3\nАдминистраторов: 2\nСредний стаж: 2.5 года\nТекучесть кадров: 12%") + +if __name__ == '__main__': + app = QApplication(sys.argv) + + # Установка стиля + app.setStyle('Fusion') + + window = FitnessApp() + window.show() + + sys.exit(app.exec()) diff --git a/control2.py b/control2.py index d55c171..49c25e4 100644 --- a/control2.py +++ b/control2.py @@ -5,10 +5,10 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QLineEdit, QComboBox, QDateEdit, QTextEdit, QMessageBox, QHeaderView, QGroupBox, - QFormLayout, QSpinBox, QCheckBox, QTimeEdit) -from PyQt6.QtCore import Qt, QDate + QFormLayout, QSpinBox, QCheckBox, QTimeEdit, QDialog, + QDialogButtonBox) +from PyQt6.QtCore import Qt, QDate, QTime from PyQt6.QtGui import QFont, QPalette, QColor -from PyQt6.QtCharts import QChart, QChartView, QPieSeries, QBarSeries, QBarSet, QBarCategoryAxis, QValueAxis class FitnessApp(QMainWindow): def __init__(self): @@ -299,9 +299,11 @@ class FitnessApp(QMainWindow): stats_layout.addLayout(stats_form) - # Правая часть - диаграмма - self.stats_chart = QChartView() - stats_layout.addWidget(self.stats_chart) + # Правая часть - таблица для статистики + self.stats_table = QTableWidget() + self.stats_table.setColumnCount(2) + self.stats_table.setHorizontalHeaderLabels(['Зона', 'Количество посещений']) + stats_layout.addWidget(self.stats_table) stats_group.setLayout(stats_layout) layout.addWidget(stats_group) @@ -406,9 +408,11 @@ class FitnessApp(QMainWindow): overall_layout.addLayout(metrics_layout) - # Правая часть - диаграмма доходов - self.revenue_chart = QChartView() - overall_layout.addWidget(self.revenue_chart) + # Правая часть - таблица доходов по типам абонементов + self.revenue_table = QTableWidget() + self.revenue_table.setColumnCount(2) + self.revenue_table.setHorizontalHeaderLabels(['Тип абонемента', 'Доход']) + overall_layout.addWidget(self.revenue_table) overall_stats_group.setLayout(overall_layout) layout.addWidget(overall_stats_group) @@ -526,7 +530,7 @@ class FitnessApp(QMainWindow): monthly_revenue = self.cursor.fetchone()[0] or 0 self.monthly_revenue_label.setText(f"{monthly_revenue:.2f} руб.") - # Диаграмма доходов + # Таблица доходов по типам абонементов self.cursor.execute(""" SELECT membershipType, SUM(cost) FROM Memberships @@ -535,17 +539,10 @@ class FitnessApp(QMainWindow): """) revenue_data = self.cursor.fetchall() - series = QPieSeries() - for membership_type, revenue in revenue_data: - series.append(membership_type, revenue) - - chart = QChart() - chart.addSeries(series) - chart.setTitle("Доходы по типам абонементов") - chart.legend().setVisible(True) - chart.legend().setAlignment(Qt.AlignmentFlag.AlignBottom) - - self.revenue_chart.setChart(chart) + self.revenue_table.setRowCount(len(revenue_data)) + for row, (membership_type, revenue) in enumerate(revenue_data): + self.revenue_table.setItem(row, 0, QTableWidgetItem(membership_type)) + self.revenue_table.setItem(row, 1, QTableWidgetItem(f"{revenue:.2f} руб.")) # Данные по тренерам self.cursor.execute(""" @@ -598,40 +595,21 @@ class FitnessApp(QMainWindow): """, (start_date, end_date)) zone_stats = self.cursor.fetchall() - series = QBarSeries() - bar_set = QBarSet("Посещения по зонам") - - categories = [] - visits = [] - - for zone, count in zone_stats: - categories.append(zone) - visits.append(count) - - bar_set.append(visits) - series.append(bar_set) - - chart = QChart() - chart.addSeries(series) - chart.setTitle(f"Посещаемость по зонам ({start_date} - {end_date})") - - axis_x = QBarCategoryAxis() - axis_x.append(categories) - chart.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom) - series.attachAxis(axis_x) - - axis_y = QValueAxis() - chart.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft) - series.attachAxis(axis_y) - - self.stats_chart.setChart(chart) + self.stats_table.setRowCount(len(zone_stats)) + for row, (zone, count) in enumerate(zone_stats): + self.stats_table.setItem(row, 0, QTableWidgetItem(zone)) + self.stats_table.setItem(row, 1, QTableWidgetItem(str(count))) -class AddClassDialog(QMessageBox): +class AddClassDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Добавить групповое занятие") self.setModal(True) + layout = QVBoxLayout() + + form_layout = QFormLayout() + self.class_name = QLineEdit() self.trainer = QComboBox() self.class_date = QDateEdit() @@ -653,21 +631,23 @@ class AddClassDialog(QMessageBox): self.hall.addItems(['Зал 1', 'Зал 2', 'Зал 3', 'Бассейн']) - layout = QFormLayout() - layout.addRow("Название:", self.class_name) - layout.addRow("Тренер:", self.trainer) - layout.addRow("Дата:", self.class_date) - layout.addRow("Время начала:", self.start_time) - layout.addRow("Время окончания:", self.end_time) - layout.addRow("Зал:", self.hall) - layout.addRow("Макс. участников:", self.max_participants) + form_layout.addRow("Название:", self.class_name) + form_layout.addRow("Тренер:", self.trainer) + form_layout.addRow("Дата:", self.class_date) + form_layout.addRow("Время начала:", self.start_time) + form_layout.addRow("Время окончания:", self.end_time) + form_layout.addRow("Зал:", self.hall) + form_layout.addRow("Макс. участников:", self.max_participants) - widget = QWidget() - widget.setLayout(layout) - self.layout().addWidget(widget, 0, 0, 1, self.layout().columnCount()) + layout.addLayout(form_layout) - self.addButton(QPushButton("Добавить"), QMessageBox.ButtonRole.AcceptRole) - self.addButton(QPushButton("Отмена"), QMessageBox.ButtonRole.RejectRole) + # Кнопки + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) + + self.setLayout(layout) def get_data(self): """Получение данных из формы""" diff --git a/control2.py.bak b/control2.py.bak new file mode 100644 index 0000000..d55c171 --- /dev/null +++ b/control2.py.bak @@ -0,0 +1,693 @@ +import sys +import sqlite3 +from datetime import datetime, date +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem, + QPushButton, QLabel, QLineEdit, QComboBox, QDateEdit, + QTextEdit, QMessageBox, QHeaderView, QGroupBox, + QFormLayout, QSpinBox, QCheckBox, QTimeEdit) +from PyQt6.QtCore import Qt, QDate +from PyQt6.QtGui import QFont, QPalette, QColor +from PyQt6.QtCharts import QChart, QChartView, QPieSeries, QBarSeries, QBarSet, QBarCategoryAxis, QValueAxis + +class FitnessApp(QMainWindow): + def __init__(self): + super().__init__() + self.initDB() + self.initUI() + + def initDB(self): + """Инициализация базы данных""" + self.conn = sqlite3.connect('fitness.db') + self.cursor = self.conn.cursor() + + # Создание таблиц + self.create_tables() + # Заполнение тестовыми данными + self.insert_sample_data() + + def create_tables(self): + """Создание таблиц базы данных""" + tables = [ + """ + CREATE TABLE IF NOT EXISTS Users ( + userID INTEGER PRIMARY KEY, + fio TEXT NOT NULL, + phone TEXT, + email TEXT, + login TEXT UNIQUE, + password TEXT, + userType TEXT, + specialization TEXT, + birthDate DATE + ) + """, + """ + CREATE TABLE IF NOT EXISTS Memberships ( + membershipID INTEGER PRIMARY KEY, + clientID INTEGER, + membershipType TEXT, + startDate DATE, + endDate DATE, + visitsTotal INTEGER, + visitsUsed INTEGER, + zones TEXT, + membershipStatus TEXT, + cost REAL, + adminID INTEGER, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (adminID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS Visits ( + visitID INTEGER PRIMARY KEY, + clientID INTEGER, + visitDate DATE, + checkInTime TIME, + checkOutTime TIME, + zone TEXT, + membershipID INTEGER, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (membershipID) REFERENCES Memberships(membershipID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS GroupClasses ( + classID INTEGER PRIMARY KEY, + className TEXT, + trainerID INTEGER, + classDate DATE, + startTime TIME, + endTime TIME, + hall TEXT, + maxParticipants INTEGER, + enrolledParticipants INTEGER, + classStatus TEXT, + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS PersonalTraining ( + trainingID INTEGER PRIMARY KEY, + clientID INTEGER, + trainerID INTEGER, + trainingDate DATE, + startTime TIME, + endTime TIME, + exercises TEXT, + notes TEXT, + progressMetrics TEXT, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS ClassRegistrations ( + registrationID INTEGER PRIMARY KEY, + classID INTEGER, + clientID INTEGER, + registrationDate DATE, + status TEXT, + FOREIGN KEY (classID) REFERENCES GroupClasses(classID), + FOREIGN KEY (clientID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS EquipmentRequests ( + requestID INTEGER PRIMARY KEY, + trainerID INTEGER, + equipment TEXT, + quantity INTEGER, + requestDate DATE, + status TEXT, + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS ShiftSwaps ( + swapID INTEGER PRIMARY KEY, + trainerID1 INTEGER, + trainerID2 INTEGER, + shiftDate DATE, + status TEXT, + FOREIGN KEY (trainerID1) REFERENCES Users(userID), + FOREIGN KEY (trainerID2) REFERENCES Users(userID) + ) + """ + ] + + for table in tables: + self.cursor.execute(table) + self.conn.commit() + + def insert_sample_data(self): + """Вставка тестовых данных""" + # Проверяем, есть ли уже данные + self.cursor.execute("SELECT COUNT(*) FROM Users") + if self.cursor.fetchone()[0] > 0: + return + + users = [ + (1, 'Сидорова Марина Петровна', '89219014567', 'director@fitness.ru', 'director1', 'pass1', 'Директор', '', '1980-05-15'), + (2, 'Романова Анна Сергеевна', '89210125678', 'admin1@fitness.ru', 'admin1', 'pass2', 'Администратор', '', '1992-08-22'), + (4, 'Яковлева Елена Викторовна', '89211236789', 'admin2@fitness.ru', 'admin2', 'pass3', 'Администратор', '', '1988-11-10'), + (7, 'Петров Дмитрий Александрович', '89212347890', 'petrov@fitness.ru', 'trainer1', 'pass4', 'Тренер', 'Силовые тренировки', '1985-03-18'), + (9, 'Смирнова Ольга Игоревна', '89213458901', 'smirnova@fitness.ru', 'trainer2', 'pass5', 'Тренер', 'Йога, Пилатес', '1990-07-25'), + (11, 'Козлов Сергей Николаевич', '89214569012', 'kozlov@fitness.ru', 'trainer3', 'pass6', 'Тренер', 'Плавание', '1987-12-05'), + (16, 'Федорова Екатерина Дмитриевна', '89161112236', 'fedorova@mail.ru', 'client1', 'pass7', 'Клиент', '', '1995-04-12'), + (21, 'Михайлов Алексей Владимирович', '89162223347', 'mikhailov@gmail.com', 'client2', 'pass8', 'Клиент', '', '1988-09-30'), + (26, 'Новикова Ирина Сергеевна', '89163334458', 'novikova@yandex.ru', 'client3', 'pass9', 'Клиент', '', '1992-06-18'), + (30, 'Соколов Игорь Петрович', '89164445569', 'sokolov@mail.ru', 'client4', 'pass10', 'Клиент', '', '1983-02-28'), + (34, 'Павлова Мария Александровна', '89165556670', 'pavlova@gmail.com', 'client5', 'pass11', 'Клиент', '', '1997-11-07') + ] + + memberships = [ + (1, 16, 'Месячный безлимит', '2024-06-01', '2024-06-30', 999, 42, 'Зал, Бассейн, Групповые', 'Активен', 5000.00, 2), + (2, 21, '12 посещений', '2024-06-05', '2024-09-05', 12, 8, 'Зал', 'Активен', 4000.00, 2), + (3, 26, 'Годовой VIP', '2024-01-10', '2025-01-10', 999, 156, 'Все зоны', 'Активен', 45000.00, 4), + (4, 30, 'Разовое посещение', '2024-06-15', '2024-06-15', 1, 1, 'Зал', 'Завершен', 500.00, 2), + (5, 34, 'Квартальный', '2024-06-01', '2024-08-31', 999, 15, 'Зал, Групповые', 'Активен', 12000.00, 4) + ] + + visits = [ + (1, 16, '2024-06-15', '08:30', '10:15', 'Тренажерный зал', 1), + (2, 21, '2024-06-15', '09:00', '10:30', 'Тренажерный зал', 2), + (3, 26, '2024-06-15', '07:00', '08:30', 'Бассейн', 3), + (4, 16, '2024-06-15', '18:00', '19:45', 'Групповое занятие', 1), + (5, 34, '2024-06-15', '19:00', '20:30', 'Групповое занятие', 5) + ] + + group_classes = [ + (1, 'Йога для начинающих', 9, '2024-06-16', '10:00', '11:00', 'Зал 2', 15, 12, 'Запланировано'), + (2, 'Силовая аэробика', 7, '2024-06-16', '18:00', '19:00', 'Зал 1', 20, 18, 'Запланировано'), + (3, 'Пилатес', 9, '2024-06-17', '11:00', '12:00', 'Зал 2', 12, 12, 'Группа заполнена'), + (4, 'Аквааэробика', 11, '2024-06-17', '15:00', '16:00', 'Бассейн', 10, 7, 'Запланировано'), + (5, 'Бокс', 7, '2024-06-18', '19:00', '20:30', 'Зал 3', 8, 5, 'Запланировано') + ] + + personal_training = [ + (1, 26, 7, '2024-06-14', '16:00', '17:00', 'Жим лежа, Приседания, Тяга блока', 'Хорошая техника', 'Жим 80кг x 8'), + (2, 16, 9, '2024-06-13', '10:00', '11:00', 'Асаны йоги, Растяжка', 'Улучшилась гибкость', ''), + (3, 21, 7, '2024-06-12', '14:00', '15:00', 'Становая тяга, Жим гантелей', 'Нужно работать над техникой', 'Становая 60кг x 6') + ] + + # Вставка данных + self.cursor.executemany("INSERT INTO Users VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", users) + self.cursor.executemany("INSERT INTO Memberships VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", memberships) + self.cursor.executemany("INSERT INTO Visits VALUES (?, ?, ?, ?, ?, ?, ?)", visits) + self.cursor.executemany("INSERT INTO GroupClasses VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", group_classes) + self.cursor.executemany("INSERT INTO PersonalTraining VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", personal_training) + self.conn.commit() + + def initUI(self): + """Инициализация пользовательского интерфейса""" + self.setWindowTitle('Фитнес-клуб - Система управления') + self.setGeometry(100, 100, 1200, 800) + + # Центральный виджет с вкладками для разных ролей + self.tabs = QTabWidget() + + # Вкладка для администратора + self.admin_tab = QWidget() + self.init_admin_tab() + self.tabs.addTab(self.admin_tab, "Администратор") + + # Вкладка для тренера + self.trainer_tab = QWidget() + self.init_trainer_tab() + self.tabs.addTab(self.trainer_tab, "Тренер") + + # Вкладка для директора + self.director_tab = QWidget() + self.init_director_tab() + self.tabs.addTab(self.director_tab, "Директор") + + self.setCentralWidget(self.tabs) + + def init_admin_tab(self): + """Инициализация вкладки администратора""" + layout = QVBoxLayout() + + # Группа управления расписанием + schedule_group = QGroupBox("Управление расписанием групповых занятий") + schedule_layout = QVBoxLayout() + + # Таблица групповых занятий + self.classes_table = QTableWidget() + self.classes_table.setColumnCount(10) + self.classes_table.setHorizontalHeaderLabels([ + 'ID', 'Название', 'Тренер', 'Дата', 'Время начала', + 'Время окончания', 'Зал', 'Макс. участников', 'Записано', 'Статус' + ]) + schedule_layout.addWidget(self.classes_table) + + # Кнопки управления + btn_layout = QHBoxLayout() + self.add_class_btn = QPushButton("Добавить занятие") + self.edit_class_btn = QPushButton("Редактировать") + self.delete_class_btn = QPushButton("Удалить") + + btn_layout.addWidget(self.add_class_btn) + btn_layout.addWidget(self.edit_class_btn) + btn_layout.addWidget(self.delete_class_btn) + + schedule_layout.addLayout(btn_layout) + schedule_group.setLayout(schedule_layout) + layout.addWidget(schedule_group) + + # Группа управления абонементами + membership_group = QGroupBox("Управление абонементами") + membership_layout = QVBoxLayout() + + self.memberships_table = QTableWidget() + self.memberships_table.setColumnCount(8) + self.memberships_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Тип', 'Начало', 'Окончание', + 'Использовано/Всего', 'Статус', 'Стоимость' + ]) + membership_layout.addWidget(self.memberships_table) + + # Кнопки для управления абонементами + membership_btn_layout = QHBoxLayout() + self.change_price_btn = QPushButton("Изменить стоимость") + self.freeze_membership_btn = QPushButton("Заморозить абонемент") + + membership_btn_layout.addWidget(self.change_price_btn) + membership_btn_layout.addWidget(self.freeze_membership_btn) + membership_layout.addLayout(membership_btn_layout) + + membership_group.setLayout(membership_layout) + layout.addWidget(membership_group) + + # Группа статистики + stats_group = QGroupBox("Статистика и отчетность") + stats_layout = QHBoxLayout() + + # Левая часть - форма для фильтров + stats_form = QFormLayout() + self.stats_start_date = QDateEdit() + self.stats_end_date = QDateEdit() + self.stats_start_date.setDate(QDate.currentDate().addMonths(-1)) + self.stats_end_date.setDate(QDate.currentDate()) + + stats_form.addRow("С:", self.stats_start_date) + stats_form.addRow("По:", self.stats_end_date) + + self.generate_stats_btn = QPushButton("Сформировать отчет") + stats_form.addRow(self.generate_stats_btn) + + stats_layout.addLayout(stats_form) + + # Правая часть - диаграмма + self.stats_chart = QChartView() + stats_layout.addWidget(self.stats_chart) + + stats_group.setLayout(stats_layout) + layout.addWidget(stats_group) + + # Загрузка данных + self.load_classes_data() + self.load_memberships_data() + + # Подключение сигналов + self.add_class_btn.clicked.connect(self.add_class) + self.generate_stats_btn.clicked.connect(self.generate_stats) + + self.admin_tab.setLayout(layout) + + def init_trainer_tab(self): + """Инициализация вкладки тренера""" + layout = QVBoxLayout() + + # Группа программ тренировок + programs_group = QGroupBox("Индивидуальные программы тренировок") + programs_layout = QVBoxLayout() + + self.programs_table = QTableWidget() + self.programs_table.setColumnCount(6) + self.programs_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Дата', 'Упражнения', 'Заметки', 'Прогресс' + ]) + programs_layout.addWidget(self.programs_table) + + programs_btn_layout = QHBoxLayout() + self.add_program_btn = QPushButton("Добавить программу") + self.edit_program_btn = QPushButton("Редактировать") + + programs_btn_layout.addWidget(self.add_program_btn) + programs_btn_layout.addWidget(self.edit_program_btn) + programs_layout.addLayout(programs_btn_layout) + + programs_group.setLayout(programs_layout) + layout.addWidget(programs_group) + + # Группа базы упражнений + exercises_group = QGroupBox("База упражнений") + exercises_layout = QVBoxLayout() + + self.exercises_table = QTableWidget() + self.exercises_table.setColumnCount(3) + self.exercises_table.setHorizontalHeaderLabels(['ID', 'Название', 'Группа мышц']) + exercises_layout.addWidget(self.exercises_table) + + exercises_btn_layout = QHBoxLayout() + self.add_exercise_btn = QPushButton("Добавить упражнение") + exercises_btn_layout.addWidget(self.add_exercise_btn) + exercises_layout.addLayout(exercises_btn_layout) + + exercises_group.setLayout(exercises_layout) + layout.addWidget(exercises_group) + + # Группа запросов оборудования + equipment_group = QGroupBox("Запросы оборудования") + equipment_layout = QVBoxLayout() + + self.equipment_table = QTableWidget() + self.equipment_table.setColumnCount(5) + self.equipment_table.setHorizontalHeaderLabels([ + 'ID', 'Оборудование', 'Количество', 'Дата запроса', 'Статус' + ]) + equipment_layout.addWidget(self.equipment_table) + + equipment_btn_layout = QHBoxLayout() + self.request_equipment_btn = QPushButton("Запросить оборудование") + equipment_btn_layout.addWidget(self.request_equipment_btn) + equipment_layout.addLayout(equipment_btn_layout) + + equipment_group.setLayout(equipment_layout) + layout.addWidget(equipment_group) + + # Загрузка данных + self.load_programs_data() + self.load_equipment_data() + + self.trainer_tab.setLayout(layout) + + def init_director_tab(self): + """Инициализация вкладки директора""" + layout = QVBoxLayout() + + # Общая статистика + overall_stats_group = QGroupBox("Общая статистика клуба") + overall_layout = QHBoxLayout() + + # Левая часть - ключевые показатели + metrics_layout = QFormLayout() + self.total_members_label = QLabel("0") + self.active_members_label = QLabel("0") + self.monthly_revenue_label = QLabel("0 руб.") + self.attendance_rate_label = QLabel("0%") + + metrics_layout.addRow("Всего клиентов:", self.total_members_label) + metrics_layout.addRow("Активных абонементов:", self.active_members_label) + metrics_layout.addRow("Доход за месяц:", self.monthly_revenue_label) + metrics_layout.addRow("Посещаемость:", self.attendance_rate_label) + + overall_layout.addLayout(metrics_layout) + + # Правая часть - диаграмма доходов + self.revenue_chart = QChartView() + overall_layout.addWidget(self.revenue_chart) + + overall_stats_group.setLayout(overall_layout) + layout.addWidget(overall_stats_group) + + # Эффективность тренеров + trainers_group = QGroupBox("Эффективность тренеров") + trainers_layout = QVBoxLayout() + + self.trainers_table = QTableWidget() + self.trainers_table.setColumnCount(6) + self.trainers_table.setHorizontalHeaderLabels([ + 'Тренер', 'Групповые занятия', 'Персональные тренировки', + 'Средняя оценка', 'Доход', 'Бонусы' + ]) + trainers_layout.addWidget(self.trainers_table) + + trainers_group.setLayout(trainers_layout) + layout.addWidget(trainers_group) + + # Финансовые показатели + finance_group = QGroupBox("Финансовые показатели") + finance_layout = QVBoxLayout() + + self.finance_table = QTableWidget() + self.finance_table.setColumnCount(4) + self.finance_table.setHorizontalHeaderLabels([ + 'Период', 'Доход', 'Расходы', 'Прибыль' + ]) + finance_layout.addWidget(self.finance_table) + + finance_group.setLayout(finance_layout) + layout.addWidget(finance_group) + + # Загрузка данных + self.load_director_data() + + self.director_tab.setLayout(layout) + + def load_classes_data(self): + """Загрузка данных о групповых занятиях""" + self.cursor.execute(""" + SELECT gc.classID, gc.className, u.fio, gc.classDate, gc.startTime, + gc.endTime, gc.hall, gc.maxParticipants, gc.enrolledParticipants, gc.classStatus + FROM GroupClasses gc + LEFT JOIN Users u ON gc.trainerID = u.userID + ORDER BY gc.classDate, gc.startTime + """) + classes = self.cursor.fetchall() + + self.classes_table.setRowCount(len(classes)) + for row, class_data in enumerate(classes): + for col, data in enumerate(class_data): + self.classes_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_memberships_data(self): + """Загрузка данных об абонементах""" + self.cursor.execute(""" + SELECT m.membershipID, u.fio, m.membershipType, m.startDate, m.endDate, + m.visitsUsed || '/' || m.visitsTotal, m.membershipStatus, m.cost + FROM Memberships m + JOIN Users u ON m.clientID = u.userID + ORDER BY m.endDate + """) + memberships = self.cursor.fetchall() + + self.memberships_table.setRowCount(len(memberships)) + for row, membership_data in enumerate(memberships): + for col, data in enumerate(membership_data): + self.memberships_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_programs_data(self): + """Загрузка данных о программах тренировок""" + self.cursor.execute(""" + SELECT pt.trainingID, u.fio, pt.trainingDate, pt.exercises, pt.notes, pt.progressMetrics + FROM PersonalTraining pt + JOIN Users u ON pt.clientID = u.userID + ORDER BY pt.trainingDate DESC + """) + programs = self.cursor.fetchall() + + self.programs_table.setRowCount(len(programs)) + for row, program_data in enumerate(programs): + for col, data in enumerate(program_data): + self.programs_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_equipment_data(self): + """Загрузка данных о запросах оборудования""" + self.cursor.execute(""" + SELECT requestID, equipment, quantity, requestDate, status + FROM EquipmentRequests + ORDER BY requestDate DESC + """) + equipment = self.cursor.fetchall() + + self.equipment_table.setRowCount(len(equipment)) + for row, equipment_data in enumerate(equipment): + for col, data in enumerate(equipment_data): + self.equipment_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_director_data(self): + """Загрузка данных для директора""" + # Общая статистика + self.cursor.execute("SELECT COUNT(*) FROM Users WHERE userType = 'Клиент'") + total_clients = self.cursor.fetchone()[0] + self.total_members_label.setText(str(total_clients)) + + self.cursor.execute("SELECT COUNT(*) FROM Memberships WHERE membershipStatus = 'Активен'") + active_memberships = self.cursor.fetchone()[0] + self.active_members_label.setText(str(active_memberships)) + + self.cursor.execute(""" + SELECT SUM(cost) FROM Memberships + WHERE strftime('%Y-%m', startDate) = strftime('%Y-%m', 'now') + """) + monthly_revenue = self.cursor.fetchone()[0] or 0 + self.monthly_revenue_label.setText(f"{monthly_revenue:.2f} руб.") + + # Диаграмма доходов + self.cursor.execute(""" + SELECT membershipType, SUM(cost) + FROM Memberships + WHERE strftime('%Y', startDate) = strftime('%Y', 'now') + GROUP BY membershipType + """) + revenue_data = self.cursor.fetchall() + + series = QPieSeries() + for membership_type, revenue in revenue_data: + series.append(membership_type, revenue) + + chart = QChart() + chart.addSeries(series) + chart.setTitle("Доходы по типам абонементов") + chart.legend().setVisible(True) + chart.legend().setAlignment(Qt.AlignmentFlag.AlignBottom) + + self.revenue_chart.setChart(chart) + + # Данные по тренерам + self.cursor.execute(""" + SELECT u.fio, + COUNT(DISTINCT gc.classID) as group_classes, + COUNT(DISTINCT pt.trainingID) as personal_trainings, + '4.5' as avg_rating, + SUM(m.cost * 0.1) as revenue, + COUNT(DISTINCT pt.trainingID) * 100 as bonuses + FROM Users u + LEFT JOIN GroupClasses gc ON u.userID = gc.trainerID + LEFT JOIN PersonalTraining pt ON u.userID = pt.trainerID + LEFT JOIN Memberships m ON pt.clientID = m.clientID + WHERE u.userType = 'Тренер' + GROUP BY u.userID, u.fio + """) + trainers = self.cursor.fetchall() + + self.trainers_table.setRowCount(len(trainers)) + for row, trainer_data in enumerate(trainers): + for col, data in enumerate(trainer_data): + self.trainers_table.setItem(row, col, QTableWidgetItem(str(data))) + + def add_class(self): + """Добавление нового группового занятия""" + dialog = AddClassDialog(self) + if dialog.exec(): + class_data = dialog.get_data() + try: + self.cursor.execute(""" + INSERT INTO GroupClasses (className, trainerID, classDate, startTime, endTime, hall, maxParticipants, enrolledParticipants, classStatus) + VALUES (?, ?, ?, ?, ?, ?, ?, 0, 'Запланировано') + """, class_data) + self.conn.commit() + self.load_classes_data() + QMessageBox.information(self, "Успех", "Занятие успешно добавлено!") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось добавить занятие: {str(e)}") + + def generate_stats(self): + """Генерация статистики посещаемости""" + start_date = self.stats_start_date.date().toString("yyyy-MM-dd") + end_date = self.stats_end_date.date().toString("yyyy-MM-dd") + + self.cursor.execute(""" + SELECT zone, COUNT(*) as visits + FROM Visits + WHERE visitDate BETWEEN ? AND ? + GROUP BY zone + """, (start_date, end_date)) + zone_stats = self.cursor.fetchall() + + series = QBarSeries() + bar_set = QBarSet("Посещения по зонам") + + categories = [] + visits = [] + + for zone, count in zone_stats: + categories.append(zone) + visits.append(count) + + bar_set.append(visits) + series.append(bar_set) + + chart = QChart() + chart.addSeries(series) + chart.setTitle(f"Посещаемость по зонам ({start_date} - {end_date})") + + axis_x = QBarCategoryAxis() + axis_x.append(categories) + chart.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom) + series.attachAxis(axis_x) + + axis_y = QValueAxis() + chart.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft) + series.attachAxis(axis_y) + + self.stats_chart.setChart(chart) + +class AddClassDialog(QMessageBox): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Добавить групповое занятие") + self.setModal(True) + + self.class_name = QLineEdit() + self.trainer = QComboBox() + self.class_date = QDateEdit() + self.start_time = QTimeEdit() + self.end_time = QTimeEdit() + self.hall = QComboBox() + self.max_participants = QSpinBox() + + self.class_date.setDate(QDate.currentDate()) + self.start_time.setTime(QTime.currentTime()) + self.end_time.setTime(QTime.currentTime().addSecs(3600)) + self.max_participants.setRange(1, 100) + + # Заполнение комбобоксов + parent.cursor.execute("SELECT userID, fio FROM Users WHERE userType = 'Тренер'") + trainers = parent.cursor.fetchall() + for trainer_id, fio in trainers: + self.trainer.addItem(fio, trainer_id) + + self.hall.addItems(['Зал 1', 'Зал 2', 'Зал 3', 'Бассейн']) + + layout = QFormLayout() + layout.addRow("Название:", self.class_name) + layout.addRow("Тренер:", self.trainer) + layout.addRow("Дата:", self.class_date) + layout.addRow("Время начала:", self.start_time) + layout.addRow("Время окончания:", self.end_time) + layout.addRow("Зал:", self.hall) + layout.addRow("Макс. участников:", self.max_participants) + + widget = QWidget() + widget.setLayout(layout) + self.layout().addWidget(widget, 0, 0, 1, self.layout().columnCount()) + + self.addButton(QPushButton("Добавить"), QMessageBox.ButtonRole.AcceptRole) + self.addButton(QPushButton("Отмена"), QMessageBox.ButtonRole.RejectRole) + + def get_data(self): + """Получение данных из формы""" + return ( + self.class_name.text(), + self.trainer.currentData(), + self.class_date.date().toString("yyyy-MM-dd"), + self.start_time.time().toString("hh:mm"), + self.end_time.time().toString("hh:mm"), + self.hall.currentText(), + self.max_participants.value() + ) + +if __name__ == '__main__': + app = QApplication(sys.argv) + + # Установка стиля + app.setStyle('Fusion') + + window = FitnessApp() + window.show() + + sys.exit(app.exec()) diff --git a/fitness.db b/fitness.db index 8472fdebbe62c0f8c7ff4c2c166eccc28304fbef..4222c95106f0eef58d0b6cca67969ec561a4c950 100644 GIT binary patch delta 212 zcmZoTz|`=7X@ayM7Xt$WHxR=B=R_T2MJ@)tCI?>r9}HX!+Zgy#`Oor5a0PS3vw5&A zV%B3^%dl-?qnUYQ2^YJ#ygXyGc}ZeYPO57~YEg1#acVJ~!R;L6>KNjx5aQ_Md5anZ02xy~ZU6uP delta 71 zcmZp8z|?SnX@ayMCj$cm7ZAe$$3z`tc}@nsszP4=9}Jv~YZ>@b`Oor5a0PS3vw5&A TV%B3^yID~{pKtKzNWx7gUYmPf78j**W+(m7Vu xTveCdl$lZS!t@IhF3f{Kg+r?^EWEJqV$?#$GCEe( zT-BD{l$lZS!t@IhF3f{Kg+r?^%(}4Q!tM(jE_Phldtt|gT^IIEek_y7lbx88Se%iU SnVnyj*z6&@-9wgfM-Twul`yLS diff --git a/service_requests.db b/service_requests.db index 65c34220f6d36e45ef7459e037396d560c71b622..2594dc98becb604ae74d76ee120164593e8e2c65 100644 GIT binary patch delta 100 zcmV-q0Gt1SpaOuP0+1U44Urr}1q}c$fU(SozY GPdq>}UMYkC delta 126 zcmZozz|^pSX@WE(&qNt#Rvreus=|#a3+-9?{xIO%(~ccVaLTLAa1{~7bv^)!n_L` dFE(D-eX;q%whKEhYyzs;3#7MhKH_iV0059XKQRCR diff --git a/service_requests_v2.db b/service_requests_v2.db new file mode 100644 index 0000000000000000000000000000000000000000..977ae1868491f7b4039dd9c19aacd347d3d1a33d GIT binary patch literal 45056 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 MBGkE$@ecmK0q8}3Gynhq literal 0 HcmV?d00001 From c6917dd85eeac84c1848a6a425d80beeab734625 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 26 Nov 2025 19:31:33 +0300 Subject: [PATCH 2/2] After Graduate Update --- fitness.db | Bin 45056 -> 45056 bytes main3.py | 2461 +++++++++++++++++ masterpol.db | Bin 94208 -> 94208 bytes ressult/.env | 6 + .../app/__pycache__/database.cpython-314.pyc | Bin 0 -> 3650 bytes ressult/app/__pycache__/main.cpython-314.pyc | Bin 0 -> 2639 bytes ressult/app/database.py | 60 + ressult/app/main.py | 48 + ressult/app/models/__init__.py | 75 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 6082 bytes ressult/app/routes/__init__.py | 5 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 368 bytes .../routes/__pycache__/auth.cpython-314.pyc | Bin 0 -> 2336 bytes .../__pycache__/calculations.cpython-314.pyc | Bin 0 -> 2150 bytes .../routes/__pycache__/config.cpython-314.pyc | Bin 0 -> 2478 bytes .../__pycache__/partners.cpython-314.pyc | Bin 0 -> 8301 bytes .../routes/__pycache__/sales.cpython-314.pyc | Bin 0 -> 4390 bytes .../routes/__pycache__/upload.cpython-314.pyc | Bin 0 -> 5317 bytes ressult/app/routes/auth.py | 45 + ressult/app/routes/calculations.py | 43 + ressult/app/routes/config.py | 32 + ressult/app/routes/partners.py | 157 ++ ressult/app/routes/sales.py | 64 + ressult/app/routes/upload.py | 103 + ressult/config.json | 24 + ressult/database_init.py | 196 ++ ressult/gui/__init__.py | 9 + .../gui/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 538 bytes .../__pycache__/login_window.cpython-314.pyc | Bin 0 -> 12047 bytes .../__pycache__/main_window.cpython-314.pyc | Bin 0 -> 29032 bytes .../material_calculator.cpython-314.pyc | Bin 0 -> 9382 bytes .../__pycache__/partner_form.cpython-314.pyc | Bin 0 -> 12301 bytes .../__pycache__/sales_history.cpython-314.pyc | Bin 0 -> 6145 bytes ressult/gui/login_window.py | 253 ++ ressult/gui/main_window | 574 ++++ ressult/gui/main_window.py | 574 ++++ ressult/gui/main_window.py.bak | 616 +++++ ressult/gui/material_calculator.py | 160 ++ ressult/gui/orders_panel.py | 344 +++ ressult/gui/partner_form.py | 193 ++ ressult/gui/partner_form.py.bak | 186 ++ ressult/gui/sales_history.py | 91 + ressult/requirements.txt | 11 + ressult/run.py | 17 + ressult/run_gui.py | 51 + robbery/master_pol-module_1_2/.gitignore | 2 + .../master_pol-module_1_2/.idea/.gitignore | 3 + robbery/master_pol-module_1_2/.idea/.name | 1 + .../inspectionProfiles/profiles_settings.xml | 6 + .../.idea/master_pol-module_1_2.iml | 10 + robbery/master_pol-module_1_2/.idea/misc.xml | 7 + .../master_pol-module_1_2/.idea/modules.xml | 8 + robbery/master_pol-module_1_2/README.md | 55 + .../app/components/edit_partner_dialog.py | 108 + .../app/components/partner_card.py | 94 + .../master_pol-module_1_2/app/database/db.py | 84 + .../app/database/script.sql | 460 +++ .../app/dto/partners_dto.py | 29 + robbery/master_pol-module_1_2/app/main.py | 11 + .../app/pages/auth_page.py | 94 + .../app/pages/partners_page.py | 130 + .../master_pol-module_1_2/app/res/colors.py | 4 + .../master_pol-module_1_2/app/res/fonts.py | 1 + .../app/res/imgs/master_pol.ico | Bin 0 -> 13342 bytes .../app/res/imgs/master_pol.png | Bin 0 -> 161727 bytes .../master_pol-module_1_2/app/res/styles.py | 35 + .../master_pol-module_1_2/requirements.txt | Bin 0 -> 138 bytes service_requests.db | Bin 36864 -> 36864 bytes service_requests_v2.db | Bin 45056 -> 45056 bytes 69 files changed, 7540 insertions(+) create mode 100644 main3.py create mode 100644 ressult/.env create mode 100644 ressult/app/__pycache__/database.cpython-314.pyc create mode 100644 ressult/app/__pycache__/main.cpython-314.pyc create mode 100644 ressult/app/database.py create mode 100644 ressult/app/main.py create mode 100644 ressult/app/models/__init__.py create mode 100644 ressult/app/models/__pycache__/__init__.cpython-314.pyc create mode 100644 ressult/app/routes/__init__.py create mode 100644 ressult/app/routes/__pycache__/__init__.cpython-314.pyc create mode 100644 ressult/app/routes/__pycache__/auth.cpython-314.pyc create mode 100644 ressult/app/routes/__pycache__/calculations.cpython-314.pyc create mode 100644 ressult/app/routes/__pycache__/config.cpython-314.pyc create mode 100644 ressult/app/routes/__pycache__/partners.cpython-314.pyc create mode 100644 ressult/app/routes/__pycache__/sales.cpython-314.pyc create mode 100644 ressult/app/routes/__pycache__/upload.cpython-314.pyc create mode 100644 ressult/app/routes/auth.py create mode 100644 ressult/app/routes/calculations.py create mode 100644 ressult/app/routes/config.py create mode 100644 ressult/app/routes/partners.py create mode 100644 ressult/app/routes/sales.py create mode 100644 ressult/app/routes/upload.py create mode 100644 ressult/config.json create mode 100644 ressult/database_init.py create mode 100644 ressult/gui/__init__.py create mode 100644 ressult/gui/__pycache__/__init__.cpython-314.pyc create mode 100644 ressult/gui/__pycache__/login_window.cpython-314.pyc create mode 100644 ressult/gui/__pycache__/main_window.cpython-314.pyc create mode 100644 ressult/gui/__pycache__/material_calculator.cpython-314.pyc create mode 100644 ressult/gui/__pycache__/partner_form.cpython-314.pyc create mode 100644 ressult/gui/__pycache__/sales_history.cpython-314.pyc create mode 100644 ressult/gui/login_window.py create mode 100644 ressult/gui/main_window create mode 100644 ressult/gui/main_window.py create mode 100644 ressult/gui/main_window.py.bak create mode 100644 ressult/gui/material_calculator.py create mode 100644 ressult/gui/orders_panel.py create mode 100644 ressult/gui/partner_form.py create mode 100644 ressult/gui/partner_form.py.bak create mode 100644 ressult/gui/sales_history.py create mode 100644 ressult/requirements.txt create mode 100644 ressult/run.py create mode 100644 ressult/run_gui.py create mode 100644 robbery/master_pol-module_1_2/.gitignore create mode 100644 robbery/master_pol-module_1_2/.idea/.gitignore create mode 100644 robbery/master_pol-module_1_2/.idea/.name create mode 100644 robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml create mode 100644 robbery/master_pol-module_1_2/.idea/misc.xml create mode 100644 robbery/master_pol-module_1_2/.idea/modules.xml create mode 100644 robbery/master_pol-module_1_2/README.md create mode 100644 robbery/master_pol-module_1_2/app/components/edit_partner_dialog.py create mode 100644 robbery/master_pol-module_1_2/app/components/partner_card.py create mode 100644 robbery/master_pol-module_1_2/app/database/db.py create mode 100644 robbery/master_pol-module_1_2/app/database/script.sql create mode 100644 robbery/master_pol-module_1_2/app/dto/partners_dto.py create mode 100644 robbery/master_pol-module_1_2/app/main.py create mode 100644 robbery/master_pol-module_1_2/app/pages/auth_page.py create mode 100644 robbery/master_pol-module_1_2/app/pages/partners_page.py create mode 100644 robbery/master_pol-module_1_2/app/res/colors.py create mode 100644 robbery/master_pol-module_1_2/app/res/fonts.py create mode 100644 robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico create mode 100644 robbery/master_pol-module_1_2/app/res/imgs/master_pol.png create mode 100644 robbery/master_pol-module_1_2/app/res/styles.py create mode 100644 robbery/master_pol-module_1_2/requirements.txt diff --git a/fitness.db b/fitness.db index 4222c95106f0eef58d0b6cca67969ec561a4c950..6659418b8f18e14e4d3e2b33eca96c5dd5f4f5a9 100644 GIT binary patch delta 131 zcmZp8z|`=7X@WE(_e2?IM(&LXOZ)}cc=s^yPvbY_yTMn&r^dTyv!cQZ-hd1?ZU%2g zMR9RWMh?dd^Dpeau;IccAl`dn$Azs57j|82xUlbH6LD-ZytST@iA delta 44 zcmZp8z|`=7X@WE(*F+g-My`zsOZ@p*`P3Nrr|}!|-QX+XQ`@YlAi}p 0: + sales_bonus = min(math.log10(sales_count + 1) * 3.0, 15.0) + else: + sales_bonus = 0.0 + + # Дополнительный бонус за крупные суммы продаж + volume_bonus = 0.0 + if total_sales > 5000000: # 5 млн руб + volume_bonus = 2.0 + elif total_sales > 2000000: # 2 млн руб + volume_bonus = 1.0 + elif total_sales > 1000000: # 1 млн руб + volume_bonus = 0.5 + + total_discount = rating_discount + sales_bonus + volume_bonus + + # Ограничение максимальной скидки 25% + return min(total_discount, 25.0) + + +# === Функция обновления скидок всех партнеров === +def update_all_partners_discounts(): + """Обновление скидок для всех партнеров на основе актуальных данных""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + # Получаем актуальные данные по продажам для каждого партнера + cursor.execute(""" + SELECT + p.partner_id, + p.rating, + COALESCE(SUM(o.final_amount), 0) as total_sales, + COUNT(o.order_id) as sales_count + FROM partners p + LEFT JOIN orders o ON p.partner_id = o.partner_id AND o.status = 'COMPLETED' + GROUP BY p.partner_id + """) + + partners_data = cursor.fetchall() + + updated_count = 0 + for partner_id, rating, total_sales, sales_count in partners_data: + # Рассчитываем новую скидку + new_discount = calculate_partner_discount( + float(rating) if rating else 0.0, + float(total_sales) if total_sales else 0.0, + sales_count + ) + + # Обновляем скидку в базе + cursor.execute(""" + UPDATE partners + SET discount_rate = ?, total_sales = ? + WHERE partner_id = ? + """, (new_discount, total_sales, partner_id)) + + updated_count += 1 + + conn.commit() + return updated_count + + except sqlite3.Error as e: + conn.rollback() + raise e + finally: + conn.close() + + +# === Функция обновления скидки конкретного партнера === +def update_partner_discount(partner_id): + """Обновление скидки для конкретного партнера""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + # Получаем актуальные данные по продажам партнера + cursor.execute(""" + SELECT + p.rating, + COALESCE(SUM(o.final_amount), 0) as total_sales, + COUNT(o.order_id) as sales_count + FROM partners p + LEFT JOIN orders o ON p.partner_id = o.partner_id AND o.status = 'COMPLETED' + WHERE p.partner_id = ? + GROUP BY p.partner_id + """, (partner_id,)) + + data = cursor.fetchone() + + if data: + rating, total_sales, sales_count = data + + # Рассчитываем новую скидку + new_discount = calculate_partner_discount( + float(rating) if rating else 0.0, + float(total_sales) if total_sales else 0.0, + sales_count + ) + + # Обновляем скидку в базе + cursor.execute(""" + UPDATE partners + SET discount_rate = ?, total_sales = ? + WHERE partner_id = ? + """, (new_discount, total_sales, partner_id)) + + conn.commit() + return new_discount + + return None + + except sqlite3.Error as e: + conn.rollback() + raise e + finally: + conn.close() + + +# === Инициализация базы данных SQLite === +def init_database(): + """Инициализация базы данных SQLite со всеми таблицами""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + # Включаем иностранные ключи + cursor.execute("PRAGMA foreign_keys = ON") + + # Таблица партнеров + cursor.execute(""" + CREATE TABLE IF NOT EXISTS partners ( + partner_id INTEGER PRIMARY KEY AUTOINCREMENT, + partner_type VARCHAR(50) NOT NULL, + company_name VARCHAR(255) NOT NULL, + legal_address TEXT, + inn VARCHAR(12) NOT NULL UNIQUE, + director_name VARCHAR(255), + phone VARCHAR(20), + email VARCHAR(255), + rating DECIMAL(3,2) CHECK (rating BETWEEN 0.00 AND 5.00), + sales_locations TEXT, + total_sales DECIMAL(15,2) DEFAULT 0, + discount_rate DECIMAL(5,2) DEFAULT 0, + created_date DATE DEFAULT CURRENT_DATE + ) + """) + + # Таблица сотрудников + cursor.execute(""" + CREATE TABLE IF NOT EXISTS employees ( + employee_id INTEGER PRIMARY KEY AUTOINCREMENT, + full_name VARCHAR(255) NOT NULL, + birth_date DATE, + passport_data TEXT, + bank_details TEXT, + has_family BOOLEAN DEFAULT FALSE, + health_info TEXT, + position VARCHAR(100), + hire_date DATE DEFAULT CURRENT_DATE, + salary DECIMAL(10,2), + is_active BOOLEAN DEFAULT TRUE + ) + """) + + # Таблица оборудования и доступов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS equipment_access ( + access_id INTEGER PRIMARY KEY AUTOINCREMENT, + employee_id INTEGER, + equipment_name VARCHAR(255) NOT NULL, + access_level VARCHAR(50), + granted_date DATE DEFAULT CURRENT_DATE, + FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE + ) + """) + + # Таблица поставщиков + cursor.execute(""" + CREATE TABLE IF NOT EXISTS suppliers ( + supplier_id INTEGER PRIMARY KEY AUTOINCREMENT, + supplier_type VARCHAR(50), + company_name VARCHAR(255) NOT NULL, + inn VARCHAR(12) NOT NULL UNIQUE, + contact_person VARCHAR(255), + phone VARCHAR(20), + email VARCHAR(255), + rating DECIMAL(3,2) CHECK (rating BETWEEN 0.00 AND 5.00), + supplied_materials TEXT + ) + """) + + # Таблица материалов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS materials ( + material_id INTEGER PRIMARY KEY AUTOINCREMENT, + material_type VARCHAR(100) NOT NULL, + material_name VARCHAR(255) NOT NULL, + supplier_id INTEGER, + package_quantity DECIMAL(10,3), + unit_of_measure VARCHAR(50), + description TEXT, + cost_per_unit DECIMAL(10,2), + current_stock DECIMAL(10,3) DEFAULT 0, + min_stock_level DECIMAL(10,3) DEFAULT 0, + image_path TEXT, + FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id) + ) + """) + + # Таблица истории изменений запасов материалов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS material_stock_history ( + history_id INTEGER PRIMARY KEY AUTOINCREMENT, + material_id INTEGER NOT NULL, + change_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + change_type VARCHAR(20) NOT NULL, -- 'IN', 'OUT', 'ADJUST' + quantity DECIMAL(10,3) NOT NULL, + reason TEXT, + employee_id INTEGER, + FOREIGN KEY (material_id) REFERENCES materials(material_id), + FOREIGN KEY (employee_id) REFERENCES employees(employee_id) + ) + """) + + # Таблица продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS products ( + product_id INTEGER PRIMARY KEY AUTOINCREMENT, + article_number VARCHAR(100) UNIQUE NOT NULL, + product_type VARCHAR(100) NOT NULL, + product_name VARCHAR(255) NOT NULL, + description TEXT, + min_partner_price DECIMAL(10,2) NOT NULL, + package_length DECIMAL(8,2), + package_width DECIMAL(8,2), + package_height DECIMAL(8,2), + net_weight DECIMAL(8,2), + gross_weight DECIMAL(8,2), + certificate_path TEXT, + standard_number VARCHAR(100), + production_time_days INTEGER DEFAULT 1, + cost_price DECIMAL(10,2), + workshop_number INTEGER, + required_workers INTEGER, + is_active BOOLEAN DEFAULT TRUE + ) + """) + + # Таблица истории цен продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS product_price_history ( + price_history_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL, + change_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + old_price DECIMAL(10,2), + new_price DECIMAL(10,2) NOT NULL, + changed_by INTEGER, + reason TEXT, + FOREIGN KEY (product_id) REFERENCES products(product_id), + FOREIGN KEY (changed_by) REFERENCES employees(employee_id) + ) + """) + + # Таблица материалов для продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS product_materials ( + product_material_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL, + material_id INTEGER NOT NULL, + material_quantity DECIMAL(10,3) NOT NULL, + FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE, + FOREIGN KEY (material_id) REFERENCES materials(material_id) + ) + """) + + # Таблица заказов (заявок) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS orders ( + order_id INTEGER PRIMARY KEY AUTOINCREMENT, + partner_id INTEGER NOT NULL, + manager_id INTEGER NOT NULL, + order_date DATE DEFAULT CURRENT_DATE, + status VARCHAR(50) DEFAULT 'NEW', -- NEW, WAITING_PREPAYMENT, IN_PRODUCTION, READY_FOR_SHIPMENT, SHIPPED, COMPLETED, CANCELLED + total_amount DECIMAL(15,2), + discount_amount DECIMAL(15,2) DEFAULT 0, + final_amount DECIMAL(15,2), + prepayment_amount DECIMAL(15,2) DEFAULT 0, + prepayment_date DATE, + full_payment_date DATE, + expected_production_date DATE, + actual_production_date DATE, + delivery_method VARCHAR(100), + delivery_address TEXT, + notes TEXT, + cancellation_reason TEXT, + FOREIGN KEY (partner_id) REFERENCES partners(partner_id), + FOREIGN KEY (manager_id) REFERENCES employees(employee_id) + ) + """) + + # Таблица позиций заказа + cursor.execute(""" + CREATE TABLE IF NOT EXISTS order_items ( + order_item_id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + quantity DECIMAL(10,3) NOT NULL, + unit_price DECIMAL(10,2) NOT NULL, + total_price DECIMAL(15,2) NOT NULL, + production_cost DECIMAL(10,2), + FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products(product_id) + ) + """) + + # Таблица продаж (история реализации) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS sales ( + sale_id INTEGER PRIMARY KEY AUTOINCREMENT, + partner_id INTEGER NOT NULL, + product_name VARCHAR(255) NOT NULL, + quantity DECIMAL(10,3) NOT NULL CHECK (quantity > 0), + sale_date DATE NOT NULL DEFAULT CURRENT_DATE, + unit_price DECIMAL(10,2), + total_amount DECIMAL(15,2), + FOREIGN KEY (partner_id) REFERENCES partners(partner_id) ON DELETE CASCADE + ) + """) + + # Таблица истории продаж партнеров (для расчета скидок) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS partner_sales_history ( + history_id INTEGER PRIMARY KEY AUTOINCREMENT, + partner_id INTEGER NOT NULL, + period_start DATE NOT NULL, + period_end DATE NOT NULL, + total_sales DECIMAL(15,2) NOT NULL, + discount_rate DECIMAL(5,2) NOT NULL, + FOREIGN KEY (partner_id) REFERENCES partners(partner_id) ON DELETE CASCADE + ) + """) + + # Таблица запасов готовой продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS finished_goods_stock ( + stock_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL, + current_stock DECIMAL(10,3) DEFAULT 0, + reserved_stock DECIMAL(10,3) DEFAULT 0, + min_stock_level DECIMAL(10,3) DEFAULT 0, + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES products(product_id) + ) + """) + + # Таблица движения готовой продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS finished_goods_movements ( + movement_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL, + movement_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + movement_type VARCHAR(20) NOT NULL, -- 'IN', 'OUT', 'RESERVE', 'UNRESERVE' + quantity DECIMAL(10,3) NOT NULL, + reference_id INTEGER, -- order_id or other reference + notes TEXT, + employee_id INTEGER, + FOREIGN KEY (product_id) REFERENCES products(product_id), + FOREIGN KEY (employee_id) REFERENCES employees(employee_id) + ) + """) + + # Добавляем тестовые данные + add_test_data(cursor) + + conn.commit() + conn.close() + + +def add_test_data(cursor): + """Добавление тестовых данных в базу""" + + # Добавляем менеджера + cursor.execute(""" + INSERT OR IGNORE INTO employees (employee_id, full_name, position, hire_date, salary, is_active) + VALUES (1, 'Иванов Алексей Петрович', 'Менеджер по продажам', '2023-01-15', 50000.00, 1) + """) + + # Добавляем пользователя + cursor.execute(""" + INSERT OR IGNORE INTO employees (employee_id, full_name, position, hire_date, salary, is_active) + VALUES (2, 'Петрова Мария Сергеевна', 'Аналитик', '2023-02-20', 45000.00, 1) + """) + + # Добавляем партнеров + partners_data = [ + (1, "ООО", "ООО «СтройГрад»", "г. Москва, ул. Ленина, 10", "770123456789", "Иван Петров", "+79001112233", "buildgrad@example.com", 4.5, "Москва, СПб", 1500000.00, 5.0), + (2, "ИП", "ИП Сидоров А.В.", "г. Казань, пр. Победы, 5", "165432109876", "Андрей Сидоров", "+79054445566", "sidorov@example.com", 4.2, "Казань", 800000.00, 3.0), + (3, "ТОО", "Торговый дом «Полимер+»", "г. Екатеринбург, ул. Мира, 22", "667890123456", "Елена Кузнецова", "+79107778899", "polymer@example.com", 4.8, "Екатеринбург, Челябинск", 2500000.00, 7.0), + ] + + for p in partners_data: + cursor.execute(""" + INSERT OR IGNORE INTO partners + (partner_id, partner_type, company_name, legal_address, inn, director_name, phone, email, rating, sales_locations, total_sales, discount_rate) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, p) + + # Добавляем поставщиков + suppliers_data = [ + (1, "ООО", "ООО «Сырье-Про»", "770987654321", "Петр Васильев", "+79012345678", "syrie@example.com", 4.7, "Древесина, клей, ламинация"), + (2, "ИП", "ИП Колесников С.И.", "163218765432", "Сергей Колесников", "+79087654321", "kolesnikov@example.com", 4.3, "ПВХ, пластификаторы"), + ] + + for s in suppliers_data: + cursor.execute(""" + INSERT OR IGNORE INTO suppliers + (supplier_id, supplier_type, company_name, inn, contact_person, phone, email, rating, supplied_materials) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, s) + + # Добавляем материалы + materials_data = [ + (1, "Древесина", "Дубовая доска", 1, 1.0, "м³", "Дубовая доска высшего сорта", 15000.00, 100.5, 20.0), + (2, "Клей", "Клей для ламината", 1, 25.0, "кг", "Водостойкий клей", 450.00, 500.0, 50.0), + (3, "ПВХ", "ПВХ пленка", 2, 50.0, "м²", "Декоративная ПВХ пленка", 320.00, 800.0, 100.0), + ] + + for m in materials_data: + cursor.execute(""" + INSERT OR IGNORE INTO materials + (material_id, material_type, material_name, supplier_id, package_quantity, unit_of_measure, description, cost_per_unit, current_stock, min_stock_level) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, m) + + # Добавляем продукцию + products_data = [ + (1, "LAM-001", "Ламинат", "Ламинат Quick-Step Classic", "Ламинат 32 класса, толщина 8мм", 1250.00, 1.2, 0.2, 0.08, 8.5, 9.2, "STD-045", 3, 850.00, 1, 2), + (2, "LAM-002", "Ламинат", "Ламинат Classen Premium", "Ламинат 33 класса, толщина 10мм", 1450.00, 1.3, 0.2, 0.09, 9.8, 10.5, "STD-048", 4, 950.00, 1, 2), + (3, "PL-001", "Плинтус", "Плинтус ПВХ белый", "Плинтус ПВХ 60мм, длина 2.5м", 350.00, 2.5, 0.06, 0.04, 0.45, 0.55, "STD-012", 1, 220.00, 2, 1), + ] + + for p in products_data: + cursor.execute(""" + INSERT OR IGNORE INTO products + (product_id, article_number, product_type, product_name, description, min_partner_price, package_length, package_width, package_height, net_weight, gross_weight, standard_number, production_time_days, cost_price, workshop_number, required_workers) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, p) + + # Добавляем связь продукции с материалами + product_materials_data = [ + (1, 1, 1, 0.05), # Ламинат Quick-Step -> Дубовая доска + (2, 1, 2, 0.8), # Ламинат Quick-Step -> Клей + (3, 1, 3, 1.2), # Ламинат Quick-Step -> ПВХ пленка + (4, 2, 1, 0.06), # Ламинат Classen -> Дубовая доска + (5, 2, 2, 1.0), # Ламинат Classen -> Клей + (6, 2, 3, 1.5), # Ламинат Classen -> ПВХ пленка + (7, 3, 3, 0.3), # Плинтус -> ПВХ пленка + ] + + for pm in product_materials_data: + cursor.execute(""" + INSERT OR IGNORE INTO product_materials (product_material_id, product_id, material_id, material_quantity) + VALUES (?, ?, ?, ?) + """, pm) + + # Добавляем запасы готовой продукции + finished_goods_data = [ + (1, 1, 500.0, 0, 50.0), + (2, 2, 300.0, 0, 30.0), + (3, 3, 1000.0, 0, 100.0), + ] + + for fg in finished_goods_data: + cursor.execute(""" + INSERT OR IGNORE INTO finished_goods_stock (stock_id, product_id, current_stock, reserved_stock, min_stock_level) + VALUES (?, ?, ?, ?, ?) + """, fg) + + # Добавляем тестовые заявки + orders_data = [ + (1, 1, 1, '2024-01-15', 'COMPLETED', 185000.00, 9250.00, 175750.00, 50000.00, '2024-01-16', '2024-01-25', '2024-01-20', '2024-01-22', 'Самовывоз', '', 'Первый заказ'), + (2, 2, 1, '2024-02-10', 'IN_PRODUCTION', 120000.00, 3600.00, 116400.00, 30000.00, '2024-02-11', None, '2024-02-25', None, 'Доставка курьером', 'г. Казань, пр. Победы, 5', 'Срочный заказ'), + (3, 3, 1, '2024-03-01', 'WAITING_PREPAYMENT', 250000.00, 17500.00, 232500.00, 0, None, None, '2024-03-20', None, 'Доставка транспортной компанией', 'г. Екатеринбург, ул. Мира, 22', 'Крупный опт'), + ] + + for order in orders_data: + cursor.execute(""" + INSERT OR IGNORE INTO orders + (order_id, partner_id, manager_id, order_date, status, total_amount, discount_amount, + final_amount, prepayment_amount, prepayment_date, full_payment_date, + expected_production_date, actual_production_date, delivery_method, delivery_address, notes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, order) + + # Добавляем позиции заказов + order_items_data = [ + (1, 1, 1, 100.0, 1250.00, 125000.00, 850.00), + (2, 1, 3, 200.0, 300.00, 60000.00, 220.00), + (3, 2, 2, 60.0, 1450.00, 87000.00, 950.00), + (4, 2, 3, 110.0, 300.00, 33000.00, 220.00), + (5, 3, 1, 150.0, 1250.00, 187500.00, 850.00), + (6, 3, 2, 40.0, 1450.00, 58000.00, 950.00), + (7, 3, 3, 150.0, 300.00, 45000.00, 220.00), + ] + + for item in order_items_data: + cursor.execute(""" + INSERT OR IGNORE INTO order_items + (order_item_id, order_id, product_id, quantity, unit_price, total_price, production_cost) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, item) + + # Добавляем больше тестовых продаж для демонстрации скидок + sales_data = [ + (1, "Ламинат Quick-Step Classic", 50.0, '2024-01-20', 1250.00, 62500.00), + (1, "Плинтус ПВХ белый", 100.0, '2024-01-20', 300.00, 30000.00), + (1, "Ламинат Classen Premium", 30.0, '2024-02-15', 1450.00, 43500.00), + (2, "Ламинат Quick-Step Classic", 25.0, '2024-01-25', 1250.00, 31250.00), + (2, "Плинтус ПВХ белый", 50.0, '2024-02-10', 300.00, 15000.00), + (3, "Ламинат Classen Premium", 100.0, '2024-01-30', 1450.00, 145000.00), + (3, "Ламинат Quick-Step Classic", 80.0, '2024-02-20', 1250.00, 100000.00), + (3, "Плинтус ПВХ белый", 200.0, '2024-03-01', 300.00, 60000.00), + ] + + for sale in sales_data: + cursor.execute(""" + INSERT OR IGNORE INTO sales + (partner_id, product_name, quantity, sale_date, unit_price, total_amount) + VALUES (?, ?, ?, ?, ?, ?) + """, sale) + + +# === Диалог авторизации === +class AuthDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Авторизация - Мастер пол") + self.setFixedSize(350, 250) + self.setStyleSheet(f""" + QDialog {{ + background-color: {APP_STYLES['primary_bg']}; + font-family: {APP_STYLES['font_family']}; + }} + QPushButton {{ + background-color: {APP_STYLES['accent_color']}; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + font-weight: bold; + }} + QPushButton:hover {{ + background-color: #5AA870; + }} + QLineEdit, QComboBox {{ + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + }} + """) + + self.authenticated = False + self.user_id = None + self.user_name = None + self.user_role = None + + layout = QVBoxLayout() + + # Заголовок + title = QLabel("Вход в систему") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 18px; font-weight: bold; margin: 10px;") + layout.addWidget(title) + + # Поля ввода + form_layout = QFormLayout() + + self.role_combo = QComboBox() + self.role_combo.addItems(["Менеджер", "Пользователь"]) + form_layout.addRow("Роль:", self.role_combo) + + self.login_edit = QLineEdit() + self.login_edit.setPlaceholderText("Введите логин") + form_layout.addRow("Логин:", self.login_edit) + + self.pass_edit = QLineEdit() + self.pass_edit.setPlaceholderText("Введите пароль") + self.pass_edit.setEchoMode(QLineEdit.EchoMode.Password) + form_layout.addRow("Пароль:", self.pass_edit) + + layout.addLayout(form_layout) + + # Кнопки + btn_layout = QHBoxLayout() + self.login_btn = QPushButton("Войти") + self.login_btn.clicked.connect(self.login) + self.cancel_btn = QPushButton("Отмена") + self.cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(self.login_btn) + btn_layout.addWidget(self.cancel_btn) + layout.addLayout(btn_layout) + + # Подсказка + hint = QLabel("Менеджер: manager/pass123\nПользователь: user/user123") + hint.setAlignment(Qt.AlignmentFlag.AlignCenter) + hint.setStyleSheet("color: #666; font-size: 12px; margin-top: 10px;") + layout.addWidget(hint) + + self.setLayout(layout) + + def login(self): + role = self.role_combo.currentText() + login = self.login_edit.text().strip() + password = self.pass_edit.text() + + if role == "Менеджер": + if login == "manager" and password == "pass123": + self.authenticated = True + self.user_id = 1 + self.user_name = "Иванов Алексей Петрович" + self.user_role = "manager" + self.accept() + else: + QMessageBox.warning(self, "Ошибка", "Неверный логин или пароль менеджера!") + else: # Пользователь + if login == "user" and password == "user123": + self.authenticated = True + self.user_id = 2 + self.user_name = "Петрова Мария Сергеевна" + self.user_role = "user" + self.accept() + else: + QMessageBox.warning(self, "Ошибка", "Неверный логин или пароль пользователя!") + + +# === Базовый класс для диалогов === +class BaseDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setStyleSheet(f""" + QDialog {{ + background-color: {APP_STYLES['primary_bg']}; + font-family: {APP_STYLES['font_family']}; + }} + QPushButton {{ + background-color: {APP_STYLES['accent_color']}; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + font-weight: bold; + }} + QPushButton:hover {{ + background-color: #5AA870; + }} + QLineEdit, QTextEdit, QComboBox, QDateEdit, QSpinBox, QDoubleSpinBox {{ + padding: 6px; + border: 1px solid #ccc; + border-radius: 4px; + }} + QGroupBox {{ + font-weight: bold; + margin-top: 10px; + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + }} + """) + + +# === Диалог партнера === +class PartnerDialog(BaseDialog): + def __init__(self, partner_data=None, parent=None): + super().__init__(parent) + self.partner_data = partner_data + title = "Добавить партнёра" if not partner_data else "Редактировать партнёра" + self.setWindowTitle(title) + self.setFixedSize(500, 550) + + layout = QVBoxLayout() + + # Основные данные + form_group = QGroupBox("Данные партнёра") + form_layout = QFormLayout() + + self.fields = { + "type": QComboBox(), + "name": QLineEdit(), + "address": QTextEdit(), + "inn": QLineEdit(), + "director": QLineEdit(), + "phone": QLineEdit(), + "email": QLineEdit(), + "rating": QDoubleSpinBox(), + "locations": QTextEdit(), + } + + # Настройка полей + self.fields["type"].addItems(["ООО", "ИП", "ТОО", "ЗАО", "ОАО", "Иное"]) + + inn_validator = QRegularExpressionValidator(QRegularExpression(r"^\d{10,12}$")) + self.fields["inn"].setValidator(inn_validator) + + self.fields["rating"].setRange(0.0, 5.0) + self.fields["rating"].setDecimals(2) + self.fields["rating"].setSingleStep(0.1) + + self.fields["address"].setMaximumHeight(70) + self.fields["locations"].setMaximumHeight(70) + + # Добавление полей в форму + form_layout.addRow("Тип партнёра *:", self.fields["type"]) + form_layout.addRow("Название компании *:", self.fields["name"]) + form_layout.addRow("Юридический адрес:", self.fields["address"]) + form_layout.addRow("ИНН *:", self.fields["inn"]) + form_layout.addRow("ФИО директора:", self.fields["director"]) + form_layout.addRow("Телефон:", self.fields["phone"]) + form_layout.addRow("Email:", self.fields["email"]) + form_layout.addRow("Рейтинг (0-5):", self.fields["rating"]) + form_layout.addRow("Места продаж:", self.fields["locations"]) + + form_group.setLayout(form_layout) + layout.addWidget(form_group) + + # Кнопки + btn_layout = QHBoxLayout() + self.save_btn = QPushButton("Сохранить") + self.save_btn.clicked.connect(self.accept) + self.cancel_btn = QPushButton("Отмена") + self.cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(self.save_btn) + btn_layout.addWidget(self.cancel_btn) + layout.addLayout(btn_layout) + + self.setLayout(layout) + + if partner_data: + self.load_data(partner_data) + + def load_data(self, data): + self.fields["type"].setCurrentText(data.get("partner_type") or "") + self.fields["name"].setText(data.get("company_name") or "") + self.fields["address"].setPlainText(data.get("legal_address") or "") + self.fields["inn"].setText(data.get("inn") or "") + self.fields["director"].setText(data.get("director_name") or "") + self.fields["phone"].setText(data.get("phone") or "") + self.fields["email"].setText(data.get("email") or "") + self.fields["rating"].setValue(float(data.get("rating") or 0.0)) + self.fields["locations"].setPlainText(data.get("sales_locations") or "") + + def get_data(self): + return { + "partner_type": self.fields["type"].currentText(), + "company_name": self.fields["name"].text().strip(), + "legal_address": self.fields["address"].toPlainText().strip() or None, + "inn": self.fields["inn"].text().strip(), + "director_name": self.fields["director"].text().strip() or None, + "phone": self.fields["phone"].text().strip() or None, + "email": self.fields["email"].text().strip() or None, + "rating": self.fields["rating"].value(), + "sales_locations": self.fields["locations"].toPlainText().strip() or None, + } + + def validate(self): + if not self.fields["name"].text().strip(): + QMessageBox.warning(self, "Ошибка", "Поле «Название компании» обязательно.") + return False + if not self.fields["inn"].text().strip(): + QMessageBox.warning(self, "Ошибка", "Поле «ИНН» обязательно.") + return False + if not self.fields["inn"].hasAcceptableInput(): + QMessageBox.warning(self, "Ошибка", "ИНН должен содержать 10 или 12 цифр.") + return False + return True + + def accept(self): + if self.validate(): + super().accept() + + +# === Диалог заказа === +class OrderDialog(BaseDialog): + def __init__(self, order_data=None, parent=None): + super().__init__(parent) + self.order_data = order_data + self.order_items = [] + + title = "Создать заявку" if not order_data else "Редактировать заявку" + self.setWindowTitle(title) + self.setMinimumSize(700, 600) + + layout = QVBoxLayout() + + # Основные данные заказа + form_group = QGroupBox("Данные заявки") + form_layout = QFormLayout() + + self.partner_combo = QComboBox() + self.status_combo = QComboBox() + self.order_date = QDateEdit() + self.expected_date = QDateEdit() + self.delivery_method = QComboBox() + self.delivery_address = QTextEdit() + self.notes = QTextEdit() + + # Настройка полей + self.status_combo.addItems(["NEW", "WAITING_PREPAYMENT", "IN_PRODUCTION", "READY_FOR_SHIPMENT", "SHIPPED", "COMPLETED", "CANCELLED"]) + self.order_date.setDate(QDate.currentDate()) + self.expected_date.setDate(QDate.currentDate().addDays(7)) + self.delivery_method.addItems(["Самовывоз", "Доставка курьером", "Доставка транспортной компанией"]) + + self.delivery_address.setMaximumHeight(60) + self.notes.setMaximumHeight(60) + + form_layout.addRow("Партнёр *:", self.partner_combo) + form_layout.addRow("Статус:", self.status_combo) + form_layout.addRow("Дата заявки:", self.order_date) + form_layout.addRow("Ожидаемая дата:", self.expected_date) + form_layout.addRow("Способ доставки:", self.delivery_method) + form_layout.addRow("Адрес доставки:", self.delivery_address) + form_layout.addRow("Примечания:", self.notes) + + form_group.setLayout(form_layout) + layout.addWidget(form_group) + + # Позиции заказа + items_group = QGroupBox("Позиции заказа") + items_layout = QVBoxLayout() + + # Таблица позиций + self.items_table = QTableWidget() + self.items_table.setColumnCount(5) + self.items_table.setHorizontalHeaderLabels(["Продукт", "Количество", "Цена", "Сумма", ""]) + self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + items_layout.addWidget(self.items_table) + + # Кнопки для позиций + items_btn_layout = QHBoxLayout() + self.add_item_btn = QPushButton("Добавить позицию") + self.add_item_btn.clicked.connect(self.add_order_item) + self.remove_item_btn = QPushButton("Удалить позицию") + self.remove_item_btn.clicked.connect(self.remove_order_item) + + items_btn_layout.addWidget(self.add_item_btn) + items_btn_layout.addWidget(self.remove_item_btn) + items_btn_layout.addStretch() + + items_layout.addLayout(items_btn_layout) + items_group.setLayout(items_layout) + layout.addWidget(items_group) + + # Итоги + totals_layout = QHBoxLayout() + self.total_label = QLabel("Итого: 0.00 руб.") + self.total_label.setStyleSheet("font-weight: bold; font-size: 14px;") + totals_layout.addStretch() + totals_layout.addWidget(self.total_label) + layout.addLayout(totals_layout) + + # Кнопки сохранения/отмены + btn_layout = QHBoxLayout() + self.save_btn = QPushButton("Сохранить заявку") + self.save_btn.clicked.connect(self.accept) + self.cancel_btn = QPushButton("Отмена") + self.cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(self.save_btn) + btn_layout.addWidget(self.cancel_btn) + layout.addLayout(btn_layout) + + self.setLayout(layout) + + self.load_partners() + + if order_data: + self.load_data(order_data) + + def load_partners(self): + """Загрузка списка партнеров в комбобокс""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT partner_id, company_name FROM partners ORDER BY company_name") + partners = cursor.fetchall() + conn.close() + + self.partner_combo.clear() + for partner_id, name in partners: + self.partner_combo.addItem(name, partner_id) + + def load_data(self, data): + """Загрузка данных заказа""" + # Загрузка основных данных + partner_index = self.partner_combo.findData(data.get("partner_id")) + if partner_index >= 0: + self.partner_combo.setCurrentIndex(partner_index) + + status_index = self.status_combo.findText(data.get("status", "NEW")) + if status_index >= 0: + self.status_combo.setCurrentIndex(status_index) + + order_date = QDate.fromString(data.get("order_date"), "yyyy-MM-dd") if data.get("order_date") else QDate.currentDate() + self.order_date.setDate(order_date) + + expected_date = QDate.fromString(data.get("expected_production_date"), "yyyy-MM-dd") if data.get("expected_production_date") else QDate.currentDate() + self.expected_date.setDate(expected_date) + + self.delivery_method.setCurrentText(data.get("delivery_method") or "") + self.delivery_address.setPlainText(data.get("delivery_address") or "") + self.notes.setPlainText(data.get("notes") or "") + + # Загрузка позиций заказа + self.load_order_items(data.get("order_id")) + + def load_order_items(self, order_id): + """Загрузка позиций заказа из БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT oi.product_id, p.product_name, oi.quantity, oi.unit_price, oi.total_price + FROM order_items oi + JOIN products p ON oi.product_id = p.product_id + WHERE oi.order_id = ? + """, (order_id,)) + + items = cursor.fetchall() + conn.close() + + self.order_items = [] + self.items_table.setRowCount(len(items)) + + for i, (product_id, product_name, quantity, unit_price, total_price) in enumerate(items): + self.items_table.setItem(i, 0, QTableWidgetItem(product_name)) + self.items_table.setItem(i, 1, QTableWidgetItem(str(quantity))) + self.items_table.setItem(i, 2, QTableWidgetItem(f"{unit_price:.2f}")) + self.items_table.setItem(i, 3, QTableWidgetItem(f"{total_price:.2f}")) + + remove_btn = QPushButton("Удалить") + remove_btn.clicked.connect(lambda checked, row=i: self.remove_specific_item(row)) + self.items_table.setCellWidget(i, 4, remove_btn) + + self.order_items.append({ + "product_id": product_id, + "product_name": product_name, + "quantity": quantity, + "unit_price": unit_price, + "total_price": total_price + }) + + self.update_totals() + + def add_order_item(self): + """Добавление новой позиции в заказ""" + dialog = OrderItemDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + item_data = dialog.get_data() + if item_data: + self.order_items.append(item_data) + self.update_items_table() + + def remove_order_item(self): + """Удаление выбранной позиции""" + current_row = self.items_table.currentRow() + if current_row >= 0 and current_row < len(self.order_items): + self.order_items.pop(current_row) + self.update_items_table() + + def remove_specific_item(self, row): + """Удаление конкретной позиции по кнопке""" + if 0 <= row < len(self.order_items): + self.order_items.pop(row) + self.update_items_table() + + def update_items_table(self): + """Обновление таблицы позиций""" + self.items_table.setRowCount(len(self.order_items)) + + for i, item in enumerate(self.order_items): + self.items_table.setItem(i, 0, QTableWidgetItem(item["product_name"])) + self.items_table.setItem(i, 1, QTableWidgetItem(str(item["quantity"]))) + self.items_table.setItem(i, 2, QTableWidgetItem(f"{item['unit_price']:.2f}")) + self.items_table.setItem(i, 3, QTableWidgetItem(f"{item['total_price']:.2f}")) + + remove_btn = QPushButton("Удалить") + remove_btn.clicked.connect(lambda checked, row=i: self.remove_specific_item(row)) + self.items_table.setCellWidget(i, 4, remove_btn) + + self.update_totals() + + def update_totals(self): + """Пересчет итоговой суммы""" + total = sum(item["total_price"] for item in self.order_items) + self.total_label.setText(f"Итого: {total:.2f} руб.") + + def get_data(self): + """Получение данных формы""" + data = { + "partner_id": self.partner_combo.currentData(), + "status": self.status_combo.currentText(), + "order_date": self.order_date.date().toString("yyyy-MM-dd"), + "expected_production_date": self.expected_date.date().toString("yyyy-MM-dd"), + "delivery_method": self.delivery_method.currentText(), + "delivery_address": self.delivery_address.toPlainText().strip(), + "notes": self.notes.toPlainText().strip(), + "total_amount": sum(item["total_price"] for item in self.order_items), + "order_items": self.order_items + } + + return data + + def validate(self): + """Валидация данных""" + if not self.partner_combo.currentData(): + QMessageBox.warning(self, "Ошибка", "Не выбран партнёр.") + return False + + if not self.order_items: + QMessageBox.warning(self, "Ошибка", "Добавьте хотя бы одну позицию в заказ.") + return False + + return True + + def accept(self): + if self.validate(): + super().accept() + + +# === Диалог позиции заказа === +class OrderItemDialog(BaseDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Добавить позицию") + self.setFixedSize(400, 300) + + layout = QVBoxLayout() + + form_layout = QFormLayout() + + self.product_combo = QComboBox() + self.quantity = QDoubleSpinBox() + self.unit_price = QDoubleSpinBox() + self.total_price = QLabel("0.00") + + # Настройка полей + self.quantity.setRange(0.1, 10000.0) + self.quantity.setDecimals(3) + self.quantity.setValue(1.0) + + self.unit_price.setRange(0.01, 100000.0) + self.unit_price.setDecimals(2) + + # Связывание сигналов для автоматического пересчета + self.quantity.valueChanged.connect(self.calculate_total) + self.unit_price.valueChanged.connect(self.calculate_total) + + form_layout.addRow("Продукт *:", self.product_combo) + form_layout.addRow("Количество *:", self.quantity) + form_layout.addRow("Цена за единицу *:", self.unit_price) + form_layout.addRow("Общая сумма:", self.total_price) + + layout.addLayout(form_layout) + + # Кнопки + btn_layout = QHBoxLayout() + self.add_btn = QPushButton("Добавить") + self.add_btn.clicked.connect(self.accept) + self.cancel_btn = QPushButton("Отмена") + self.cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(self.add_btn) + btn_layout.addWidget(self.cancel_btn) + layout.addLayout(btn_layout) + + self.setLayout(layout) + + self.load_products() + + def load_products(self): + """Загрузка списка продукции""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT product_id, product_name, min_partner_price FROM products WHERE is_active = 1") + products = cursor.fetchall() + conn.close() + + self.product_combo.clear() + for product_id, product_name, min_price in products: + self.product_combo.addItem(f"{product_name} ({min_price:.2f} руб.)", (product_id, min_price)) + + if self.product_combo.count() > 0: + self.product_combo.currentIndexChanged.connect(self.product_changed) + self.product_changed(0) # Установить цену для первого товара + + def product_changed(self, index): + """Обработчик изменения выбранного продукта""" + if index >= 0: + product_data = self.product_combo.currentData() + if product_data: + product_id, min_price = product_data + self.unit_price.setValue(float(min_price)) + + def calculate_total(self): + """Пересчет общей суммы""" + total = self.quantity.value() * self.unit_price.value() + self.total_price.setText(f"{total:.2f}") + + def get_data(self): + """Получение данных позиции""" + if self.product_combo.currentIndex() < 0: + return None + + product_data = self.product_combo.currentData() + if not product_data: + return None + + product_id, min_price = product_data + quantity = self.quantity.value() + unit_price = self.unit_price.value() + total_price = quantity * unit_price + + return { + "product_id": product_id, + "product_name": self.product_combo.currentText().split(' (')[0], # Извлекаем название без цены + "quantity": quantity, + "unit_price": unit_price, + "total_price": total_price + } + + def validate(self): + """Валидация данных""" + if self.product_combo.currentIndex() < 0: + QMessageBox.warning(self, "Ошибка", "Не выбран продукт.") + return False + + if self.quantity.value() <= 0: + QMessageBox.warning(self, "Ошибка", "Количество должно быть больше 0.") + return False + + if self.unit_price.value() <= 0: + QMessageBox.warning(self, "Ошибка", "Цена должна быть больше 0.") + return False + + return True + + def accept(self): + if self.validate(): + super().accept() + + +# === Основное окно приложения === +class MainWindow(QMainWindow): + def __init__(self, user_id, user_name, user_role): + super().__init__() + self.user_id = user_id + self.user_name = user_name + self.user_role = user_role + + role_display = "Менеджер" if user_role == "manager" else "Пользователь" + self.setWindowTitle(f"Мастер пол - Система управления ({role_display}: {user_name})") + self.setMinimumSize(1200, 700) + + # Установка стилей + self.setStyleSheet(f""" + QMainWindow {{ + background-color: {APP_STYLES['primary_bg']}; + font-family: {APP_STYLES['font_family']}; + }} + QTabWidget::pane {{ + border: 1px solid #C2C7CB; + background-color: {APP_STYLES['primary_bg']}; + }} + QTabBar::tab {{ + background-color: {APP_STYLES['secondary_bg']}; + border: 1px solid #C2C7CB; + padding: 8px 15px; + margin-right: 2px; + }} + QTabBar::tab:selected {{ + background-color: {APP_STYLES['accent_color']}; + color: white; + }} + QPushButton {{ + background-color: {APP_STYLES['accent_color']}; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + font-weight: bold; + }} + QPushButton:hover {{ + background-color: #5AA870; + }} + QPushButton:disabled {{ + background-color: #CCCCCC; + color: #666666; + }} + QTableWidget {{ + gridline-color: #D0D0D0; + selection-background-color: {APP_STYLES['accent_color']}; + }} + QHeaderView::section {{ + background-color: {APP_STYLES['secondary_bg']}; + padding: 5px; + border: 1px solid #D0D0D0; + font-weight: bold; + }} + """) + + self.init_ui() + self.load_initial_data() + + def init_ui(self): + """Инициализация пользовательского интерфейса""" + central_widget = QWidget() + self.setCentralWidget(central_widget) + + layout = QVBoxLayout() + + # Заголовок + header_layout = QHBoxLayout() + title = QLabel("Система управления производством «Мастер пол»") + title.setStyleSheet("font-size: 20px; font-weight: bold; color: #333;") + header_layout.addWidget(title) + header_layout.addStretch() + + user_label = QLabel(f"{'Менеджер' if self.user_role == 'manager' else 'Пользователь'}: {self.user_name}") + user_label.setStyleSheet("color: #666;") + header_layout.addWidget(user_label) + + layout.addLayout(header_layout) + + # Вкладки + self.tabs = QTabWidget() + + # Создаем вкладки + self.partners_tab = self.create_partners_tab() + self.orders_tab = self.create_orders_tab() + self.products_tab = self.create_products_tab() + self.employees_tab = self.create_employees_tab() + self.materials_tab = self.create_materials_tab() + + self.tabs.addTab(self.partners_tab, "Партнёры") + self.tabs.addTab(self.orders_tab, "Заявки") + self.tabs.addTab(self.products_tab, "Продукция") + self.tabs.addTab(self.employees_tab, "Сотрудники") + self.tabs.addTab(self.materials_tab, "Материалы") + + layout.addWidget(self.tabs) + central_widget.setLayout(layout) + + # Настройка прав доступа в зависимости от роли + self.setup_permissions() + + def setup_permissions(self): + """Настройка прав доступа в зависимости от роли пользователя""" + is_manager = self.user_role == "manager" + + # Партнеры + self.add_partner_btn.setEnabled(is_manager) + self.edit_partner_btn.setEnabled(is_manager) + self.delete_partner_btn.setEnabled(is_manager) + self.update_discounts_btn.setEnabled(is_manager) + + # Заявки + self.add_order_btn.setEnabled(is_manager) + self.edit_order_btn.setEnabled(is_manager) + self.update_status_btn.setEnabled(is_manager) + self.delete_order_btn.setEnabled(is_manager) + + # Продукция + self.add_product_btn.setEnabled(is_manager) + self.edit_product_btn.setEnabled(is_manager) + self.delete_product_btn.setEnabled(is_manager) + + # Сотрудники + self.add_employee_btn.setEnabled(is_manager) + self.edit_employee_btn.setEnabled(is_manager) + self.delete_employee_btn.setEnabled(is_manager) + + # Материалы + self.add_material_btn.setEnabled(is_manager) + self.edit_material_btn.setEnabled(is_manager) + self.delete_material_btn.setEnabled(is_manager) + + def create_partners_tab(self): + """Создание вкладки партнеров""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_partner_btn = QPushButton("➕ Добавить партнёра") + self.edit_partner_btn = QPushButton("✏️ Редактировать") + self.view_sales_btn = QPushButton("📊 История продаж") + self.delete_partner_btn = QPushButton("🗑 Удалить") + self.update_discounts_btn = QPushButton("🎯 Обновить скидки") # Новая кнопка + self.refresh_partners_btn = QPushButton("🔄 Обновить") + + self.add_partner_btn.clicked.connect(self.add_partner) + self.edit_partner_btn.clicked.connect(self.edit_partner) + self.view_sales_btn.clicked.connect(self.view_sales_history) + self.delete_partner_btn.clicked.connect(self.delete_partner) + self.update_discounts_btn.clicked.connect(self.update_partner_discounts) # Новый обработчик + self.refresh_partners_btn.clicked.connect(self.load_partners) + + control_layout.addWidget(self.add_partner_btn) + control_layout.addWidget(self.edit_partner_btn) + control_layout.addWidget(self.view_sales_btn) + control_layout.addWidget(self.delete_partner_btn) + control_layout.addWidget(self.update_discounts_btn) # Добавляем кнопку в layout + control_layout.addStretch() + control_layout.addWidget(self.refresh_partners_btn) + + layout.addLayout(control_layout) + + # Таблица партнеров + self.partners_table = QTableWidget() + self.partners_table.setColumnCount(10) + self.partners_table.setHorizontalHeaderLabels([ + "ID", "Тип", "Компания", "ИНН", "Директор", "Телефон", "Email", "Рейтинг", "Общие продажи", "Скидка %" + ]) + + # Настройка таблицы + header = self.partners_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(7, QHeaderView.ResizeMode.ResizeToContents) + + self.partners_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.partners_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.partners_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + # Двойной клик для редактирования + self.partners_table.doubleClicked.connect(self.edit_partner) + + layout.addWidget(self.partners_table) + widget.setLayout(layout) + + return widget + + def create_orders_tab(self): + """Создание вкладки заявок""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_order_btn = QPushButton("➕ Новая заявка") + self.edit_order_btn = QPushButton("✏️ Редактировать") + self.view_order_btn = QPushButton("👁 Просмотреть") + self.update_status_btn = QPushButton("🔄 Обновить статус") + self.delete_order_btn = QPushButton("🗑 Удалить") + self.refresh_orders_btn = QPushButton("🔄 Обновить") + + self.add_order_btn.clicked.connect(self.add_order) + self.edit_order_btn.clicked.connect(self.edit_order) + self.view_order_btn.clicked.connect(self.view_order) + self.update_status_btn.clicked.connect(self.update_order_status) + self.delete_order_btn.clicked.connect(self.delete_order) + self.refresh_orders_btn.clicked.connect(self.load_orders) + + control_layout.addWidget(self.add_order_btn) + control_layout.addWidget(self.edit_order_btn) + control_layout.addWidget(self.view_order_btn) + control_layout.addWidget(self.update_status_btn) + control_layout.addWidget(self.delete_order_btn) + control_layout.addStretch() + control_layout.addWidget(self.refresh_orders_btn) + + layout.addLayout(control_layout) + + # Таблица заявок + self.orders_table = QTableWidget() + self.orders_table.setColumnCount(8) + self.orders_table.setHorizontalHeaderLabels([ + "ID", "Партнёр", "Дата", "Статус", "Сумма", "Скидка", "Итог", "Менеджер" + ]) + + # Настройка таблицы + header = self.orders_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) + + self.orders_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.orders_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.orders_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + layout.addWidget(self.orders_table) + widget.setLayout(layout) + + return widget + + def create_products_tab(self): + """Создание вкладки продукции""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_product_btn = QPushButton("➕ Добавить продукт") + self.edit_product_btn = QPushButton("✏️ Редактировать") + self.view_materials_btn = QPushButton("📋 Состав продукции") + self.delete_product_btn = QPushButton("🗑 Удалить") + self.refresh_products_btn = QPushButton("🔄 Обновить") + + self.add_product_btn.clicked.connect(self.add_product) + self.edit_product_btn.clicked.connect(self.edit_product) + self.view_materials_btn.clicked.connect(self.view_product_materials) + self.delete_product_btn.clicked.connect(self.delete_product) + self.refresh_products_btn.clicked.connect(self.load_products) + + control_layout.addWidget(self.add_product_btn) + control_layout.addWidget(self.edit_product_btn) + control_layout.addWidget(self.view_materials_btn) + control_layout.addWidget(self.delete_product_btn) + control_layout.addStretch() + control_layout.addWidget(self.refresh_products_btn) + + layout.addLayout(control_layout) + + # Таблица продукции + self.products_table = QTableWidget() + self.products_table.setColumnCount(10) + self.products_table.setHorizontalHeaderLabels([ + "ID", "Артикул", "Тип", "Наименование", "Мин. цена", "Вес нетто", "Вес брутто", "Время пр-ва", "Себестоимость", "Цех" + ]) + + # Настройка таблицы + header = self.products_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) + + self.products_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.products_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.products_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + layout.addWidget(self.products_table) + widget.setLayout(layout) + + return widget + + def create_employees_tab(self): + """Создание вкладки сотрудников""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_employee_btn = QPushButton("➕ Добавить сотрудника") + self.edit_employee_btn = QPushButton("✏️ Редактировать") + self.view_access_btn = QPushButton("🔧 Доступ к оборудованию") + self.delete_employee_btn = QPushButton("🗑 Удалить") + self.refresh_employees_btn = QPushButton("🔄 Обновить") + + self.add_employee_btn.clicked.connect(self.add_employee) + self.edit_employee_btn.clicked.connect(self.edit_employee) + self.view_access_btn.clicked.connect(self.view_equipment_access) + self.delete_employee_btn.clicked.connect(self.delete_employee) + self.refresh_employees_btn.clicked.connect(self.load_employees) + + control_layout.addWidget(self.add_employee_btn) + control_layout.addWidget(self.edit_employee_btn) + control_layout.addWidget(self.view_access_btn) + control_layout.addWidget(self.delete_employee_btn) + control_layout.addStretch() + control_layout.addWidget(self.refresh_employees_btn) + + layout.addLayout(control_layout) + + # Таблица сотрудников + self.employees_table = QTableWidget() + self.employees_table.setColumnCount(8) + self.employees_table.setHorizontalHeaderLabels([ + "ID", "ФИО", "Должность", "Дата найма", "Зарплата", "Дата рождения", "Семья", "Статус" + ]) + + # Настройка таблицы + header = self.employees_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + + self.employees_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.employees_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.employees_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + layout.addWidget(self.employees_table) + widget.setLayout(layout) + + return widget + + def create_materials_tab(self): + """Создание вкладки материалов""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_material_btn = QPushButton("➕ Добавить материал") + self.edit_material_btn = QPushButton("✏️ Редактировать") + self.view_stock_btn = QPushButton("📊 История запасов") + self.delete_material_btn = QPushButton("🗑 Удалить") + self.refresh_materials_btn = QPushButton("🔄 Обновить") + + self.add_material_btn.clicked.connect(self.add_material) + self.edit_material_btn.clicked.connect(self.edit_material) + self.view_stock_btn.clicked.connect(self.view_stock_history) + self.delete_material_btn.clicked.connect(self.delete_material) + self.refresh_materials_btn.clicked.connect(self.load_materials) + + control_layout.addWidget(self.add_material_btn) + control_layout.addWidget(self.edit_material_btn) + control_layout.addWidget(self.view_stock_btn) + control_layout.addWidget(self.delete_material_btn) + control_layout.addStretch() + control_layout.addWidget(self.refresh_materials_btn) + + layout.addLayout(control_layout) + + # Таблица материалов + self.materials_table = QTableWidget() + self.materials_table.setColumnCount(9) + self.materials_table.setHorizontalHeaderLabels([ + "ID", "Тип", "Наименование", "Поставщик", "Ед. изм.", "Цена", "Текущий запас", "Мин. запас", "Описание" + ]) + + # Настройка таблицы + header = self.materials_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) + + self.materials_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.materials_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.materials_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + layout.addWidget(self.materials_table) + widget.setLayout(layout) + + return widget + + def load_initial_data(self): + """Загрузка начальных данных во все таблицы""" + self.load_partners() + self.load_orders() + self.load_products() + self.load_employees() + self.load_materials() + + def load_partners(self): + """Загрузка данных партнеров""" + self.partners_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT partner_id, partner_type, company_name, inn, director_name, + phone, email, rating, total_sales, discount_rate + FROM partners + ORDER BY company_name + """) + rows = cursor.fetchall() + conn.close() + + self.partners_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j in [7, 8, 9] and val is not None: # рейтинг, продажи, скидка + if j == 7: # рейтинг + item = QTableWidgetItem(f"{float(val):.2f}") + elif j == 8: # продажи + item = QTableWidgetItem(f"{float(val):.2f}") + else: # скидка + item = QTableWidgetItem(f"{float(val):.1f}%") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.partners_table.setItem(i, j, item) + + def load_orders(self): + """Загрузка данных заявок""" + self.orders_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT o.order_id, p.company_name, o.order_date, o.status, + o.total_amount, o.discount_amount, o.final_amount, e.full_name + FROM orders o + JOIN partners p ON o.partner_id = p.partner_id + JOIN employees e ON o.manager_id = e.employee_id + ORDER BY o.order_date DESC + """) + rows = cursor.fetchall() + conn.close() + + self.orders_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j in [4, 5, 6] and val is not None: # суммы + item = QTableWidgetItem(f"{float(val):.2f}") + elif j == 3: # статус + status_text = { + 'NEW': 'Новая', + 'WAITING_PREPAYMENT': 'Ожидает предоплаты', + 'IN_PRODUCTION': 'В производстве', + 'READY_FOR_SHIPMENT': 'Готов к отгрузке', + 'SHIPPED': 'Отгружен', + 'COMPLETED': 'Завершен', + 'CANCELLED': 'Отменен' + }.get(val, val) + item = QTableWidgetItem(status_text) + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.orders_table.setItem(i, j, item) + + def load_products(self): + """Загрузка данных продукции""" + self.products_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT product_id, article_number, product_type, product_name, + min_partner_price, net_weight, gross_weight, production_time_days, + cost_price, workshop_number + FROM products + WHERE is_active = 1 + ORDER BY product_type, product_name + """) + rows = cursor.fetchall() + conn.close() + + self.products_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j in [4, 5, 6, 8] and val is not None: # цены и веса + item = QTableWidgetItem(f"{float(val):.2f}") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.products_table.setItem(i, j, item) + + def load_employees(self): + """Загрузка данных сотрудников""" + self.employees_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT employee_id, full_name, position, hire_date, salary, + birth_date, has_family, is_active + FROM employees + ORDER BY full_name + """) + rows = cursor.fetchall() + conn.close() + + self.employees_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j == 4 and val is not None: # зарплата + item = QTableWidgetItem(f"{float(val):.2f}") + elif j == 6: # семья + item = QTableWidgetItem("Да" if val else "Нет") + elif j == 7: # статус + item = QTableWidgetItem("Активен" if val else "Неактивен") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.employees_table.setItem(i, j, item) + + def load_materials(self): + """Загрузка данных материалов""" + self.materials_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT m.material_id, m.material_type, m.material_name, + s.company_name, m.unit_of_measure, m.cost_per_unit, + m.current_stock, m.min_stock_level, m.description + FROM materials m + LEFT JOIN suppliers s ON m.supplier_id = s.supplier_id + ORDER BY m.material_type, m.material_name + """) + rows = cursor.fetchall() + conn.close() + + self.materials_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j in [5, 6, 7] and val is not None: # цена и запасы + item = QTableWidgetItem(f"{float(val):.2f}") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.materials_table.setItem(i, j, item) + + def get_selected_partner_id(self): + """Получение ID выбранного партнера""" + selected = self.partners_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите партнёра в таблице.") + return None + + row = selected[0].row() + item = self.partners_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + def get_selected_order_id(self): + """Получение ID выбранной заявки""" + selected = self.orders_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите заявку в таблице.") + return None + + row = selected[0].row() + item = self.orders_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + def get_selected_product_id(self): + """Получение ID выбранной продукции""" + selected = self.products_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите продукт в таблице.") + return None + + row = selected[0].row() + item = self.products_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + def get_selected_employee_id(self): + """Получение ID выбранного сотрудника""" + selected = self.employees_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите сотрудника в таблице.") + return None + + row = selected[0].row() + item = self.employees_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + def get_selected_material_id(self): + """Получение ID выбранного материала""" + selected = self.materials_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите материал в таблице.") + return None + + row = selected[0].row() + item = self.materials_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + # === Методы для партнеров === + def add_partner(self): + """Добавление нового партнера""" + dialog = PartnerDialog() + if dialog.exec() == QDialog.DialogCode.Accepted: + data = dialog.get_data() + self.save_partner_to_db(data) + + def edit_partner(self): + """Редактирование выбранного партнера""" + partner_id = self.get_selected_partner_id() + if not partner_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT * FROM partners WHERE partner_id = ?", (partner_id,)) + row = cursor.fetchone() + conn.close() + + if not row: + QMessageBox.warning(self, "Ошибка", "Партнёр не найден.") + return + + # Преобразование в словарь + columns = [description[0] for description in cursor.description] + partner_data = dict(zip(columns, row)) + + dialog = PartnerDialog(partner_data) + if dialog.exec() == QDialog.DialogCode.Accepted: + data = dialog.get_data() + data["partner_id"] = partner_id + self.update_partner_in_db(data) + + def view_sales_history(self): + """Просмотр истории продаж партнера""" + partner_id = self.get_selected_partner_id() + if not partner_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + # Получение названия компании + cursor.execute("SELECT company_name FROM partners WHERE partner_id = ?", (partner_id,)) + partner_name = cursor.fetchone()[0] + + # Получение истории продаж + cursor.execute(""" + SELECT product_name, quantity, unit_price, total_amount, sale_date + FROM sales + WHERE partner_id = ? + ORDER BY sale_date DESC + """, (partner_id,)) + sales = cursor.fetchall() + conn.close() + + # Создание диалога для отображения истории + dialog = BaseDialog(self) + dialog.setWindowTitle(f"История продаж: {partner_name}") + dialog.setFixedSize(600, 400) + + layout = QVBoxLayout() + + # Таблица продаж + table = QTableWidget() + table.setColumnCount(5) + table.setHorizontalHeaderLabels(["Продукт", "Количество", "Цена", "Сумма", "Дата"]) + table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + + table.setRowCount(len(sales)) + for i, sale in enumerate(sales): + for j, val in enumerate(sale): + if j in [1, 2, 3] and val is not None: # количества и цены + if j == 1: # количество + item = QTableWidgetItem(f"{float(val):.3f}") + else: # цены + item = QTableWidgetItem(f"{float(val):.2f}") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + table.setItem(i, j, item) + + layout.addWidget(table) + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.setLayout(layout) + dialog.exec() + + def delete_partner(self): + """Удаление выбранного партнера""" + partner_id = self.get_selected_partner_id() + if not partner_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить этого партнёра?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("DELETE FROM partners WHERE partner_id = ?", (partner_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Партнёр удалён.") + self.load_partners() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить партнёра: {e}") + finally: + conn.close() + + def update_partner_discounts(self): + """Обновление скидок для всех партнеров""" + if self.user_role != "manager": + QMessageBox.warning(self, "Ошибка", "Только менеджер может обновлять скидки.") + return + + reply = QMessageBox.question( + self, "Подтверждение", + "Обновить скидки для всех партнеров на основе текущих продаж и рейтингов?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + try: + updated_count = update_all_partners_discounts() + QMessageBox.information( + self, "Успех", + f"Скидки обновлены для {updated_count} партнеров.\n" + f"Скидки рассчитываются по формуле:\n" + f"- Рейтинг × 2% (макс. 10%)\n" + f"- Бонус за количество продаж (макс. 15%)\n" + f"- Бонус за объем продаж (до 2%)\n" + f"- Максимальная скидка: 25%" + ) + self.load_partners() + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось обновить скидки: {e}") + + def save_partner_to_db(self, data): + """Сохранение нового партнера в БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT INTO partners + (partner_type, company_name, legal_address, inn, director_name, phone, email, rating, sales_locations) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + data["partner_type"], + data["company_name"], + data["legal_address"], + data["inn"], + data["director_name"], + data["phone"], + data["email"], + data["rating"], + data["sales_locations"] + )) + conn.commit() + QMessageBox.information(self, "Успех", "Партнёр добавлен.") + self.load_partners() + except sqlite3.IntegrityError: + QMessageBox.critical(self, "Ошибка", "Партнёр с таким ИНН уже существует.") + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось добавить партнёра: {e}") + finally: + conn.close() + + def update_partner_in_db(self, data): + """Обновление данных партнера в БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + cursor.execute(""" + UPDATE partners SET + partner_type = ?, company_name = ?, legal_address = ?, inn = ?, + director_name = ?, phone = ?, email = ?, rating = ?, sales_locations = ? + WHERE partner_id = ? + """, ( + data["partner_type"], + data["company_name"], + data["legal_address"], + data["inn"], + data["director_name"], + data["phone"], + data["email"], + data["rating"], + data["sales_locations"], + data["partner_id"] + )) + conn.commit() + QMessageBox.information(self, "Успех", "Данные партнёра обновлены.") + self.load_partners() + except sqlite3.IntegrityError: + QMessageBox.critical(self, "Ошибка", "Партнёр с таким ИНН уже существует.") + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось обновить данные: {e}") + finally: + conn.close() + + # === Методы для заявок === + def add_order(self): + """Создание новой заявки""" + dialog = OrderDialog() + if dialog.exec() == QDialog.DialogCode.Accepted: + data = dialog.get_data() + self.save_order_to_db(data) + + def edit_order(self): + """Редактирование заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)) + row = cursor.fetchone() + conn.close() + + if not row: + QMessageBox.warning(self, "Ошибка", "Заявка не найдена.") + return + + # Преобразование в словарь + columns = [description[0] for description in cursor.description] + order_data = dict(zip(columns, row)) + + dialog = OrderDialog(order_data) + if dialog.exec() == QDialog.DialogCode.Accepted: + data = dialog.get_data() + data["order_id"] = order_id + self.update_order_in_db(data) + + def view_order(self): + """Просмотр деталей заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + # Получение основной информации о заявке + cursor.execute(""" + SELECT o.*, p.company_name, e.full_name + FROM orders o + JOIN partners p ON o.partner_id = p.partner_id + JOIN employees e ON o.manager_id = e.employee_id + WHERE o.order_id = ? + """, (order_id,)) + order = cursor.fetchone() + + if not order: + QMessageBox.warning(self, "Ошибка", "Заявка не найдена.") + conn.close() + return + + # Получение позиций заявки + cursor.execute(""" + SELECT p.product_name, oi.quantity, oi.unit_price, oi.total_price + FROM order_items oi + JOIN products p ON oi.product_id = p.product_id + WHERE oi.order_id = ? + """, (order_id,)) + items = cursor.fetchall() + conn.close() + + # Создание диалога для просмотра + dialog = BaseDialog(self) + dialog.setWindowTitle(f"Детали заявки #{order_id}") + dialog.setFixedSize(600, 500) + + layout = QVBoxLayout() + + # Основная информация + info_group = QGroupBox("Информация о заявке") + info_layout = QFormLayout() + + info_layout.addRow("Номер заявки:", QLabel(str(order[0]))) + info_layout.addRow("Партнёр:", QLabel(order[16])) # company_name + info_layout.addRow("Менеджер:", QLabel(order[17])) # full_name + info_layout.addRow("Дата заявки:", QLabel(order[3])) + info_layout.addRow("Статус:", QLabel(order[4])) + info_layout.addRow("Общая сумма:", QLabel(f"{order[5]:.2f} руб.")) + info_layout.addRow("Скидка:", QLabel(f"{order[6]:.2f} руб.")) + info_layout.addRow("Итоговая сумма:", QLabel(f"{order[7]:.2f} руб.")) + + info_group.setLayout(info_layout) + layout.addWidget(info_group) + + # Позиции заявки + items_group = QGroupBox("Позиции заявки") + items_layout = QVBoxLayout() + + table = QTableWidget() + table.setColumnCount(4) + table.setHorizontalHeaderLabels(["Продукт", "Количество", "Цена", "Сумма"]) + table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + + table.setRowCount(len(items)) + total = 0 + for i, item in enumerate(items): + for j, val in enumerate(item): + if j in [1, 2, 3] and val is not None: + item_text = f"{float(val):.2f}" if j in [2, 3] else f"{float(val):.3f}" + table.setItem(i, j, QTableWidgetItem(item_text)) + else: + table.setItem(i, j, QTableWidgetItem(str(val))) + total += float(item[3]) + + items_layout.addWidget(table) + items_group.setLayout(items_layout) + layout.addWidget(items_group) + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.setLayout(layout) + dialog.exec() + + def update_order_status(self): + """Обновление статуса заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT status FROM orders WHERE order_id = ?", (order_id,)) + current_status = cursor.fetchone()[0] + conn.close() + + dialog = BaseDialog(self) + dialog.setWindowTitle("Обновление статуса заявки") + dialog.setFixedSize(300, 150) + + layout = QVBoxLayout() + + layout.addWidget(QLabel("Текущий статус: " + current_status)) + + status_combo = QComboBox() + status_combo.addItems(["NEW", "WAITING_PREPAYMENT", "IN_PRODUCTION", "READY_FOR_SHIPMENT", "SHIPPED", "COMPLETED", "CANCELLED"]) + status_combo.setCurrentText(current_status) + layout.addWidget(QLabel("Новый статус:")) + layout.addWidget(status_combo) + + btn_layout = QHBoxLayout() + save_btn = QPushButton("Сохранить") + cancel_btn = QPushButton("Отмена") + + btn_layout.addWidget(save_btn) + btn_layout.addWidget(cancel_btn) + layout.addLayout(btn_layout) + + dialog.setLayout(layout) + + def save_status(): + new_status = status_combo.currentText() + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("UPDATE orders SET status = ? WHERE order_id = ?", (new_status, order_id)) + conn.commit() + conn.close() + QMessageBox.information(self, "Успех", "Статус заявки обновлен.") + self.load_orders() + dialog.accept() + + save_btn.clicked.connect(save_status) + cancel_btn.clicked.connect(dialog.reject) + + dialog.exec() + + def delete_order(self): + """Удаление заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить эту заявку?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("DELETE FROM orders WHERE order_id = ?", (order_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Заявка удалена.") + self.load_orders() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить заявку: {e}") + finally: + conn.close() + + def save_order_to_db(self, data): + """Сохранение заявки в БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + # Вставка основной информации о заявке + cursor.execute(""" + INSERT INTO orders + (partner_id, manager_id, order_date, status, expected_production_date, + delivery_method, delivery_address, notes, total_amount, final_amount) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + data["partner_id"], + self.user_id, # ID текущего менеджера + data["order_date"], + data["status"], + data["expected_production_date"], + data["delivery_method"], + data["delivery_address"], + data["notes"], + data["total_amount"], + data["total_amount"] # final_amount = total_amount (без скидки в демо) + )) + + order_id = cursor.lastrowid + + # Вставка позиций заявки + for item in data["order_items"]: + cursor.execute(""" + INSERT INTO order_items + (order_id, product_id, quantity, unit_price, total_price) + VALUES (?, ?, ?, ?, ?) + """, ( + order_id, + item["product_id"], + item["quantity"], + item["unit_price"], + item["total_price"] + )) + + conn.commit() + QMessageBox.information(self, "Успех", "Заявка создана.") + self.load_orders() + + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось создать заявку: {e}") + finally: + conn.close() + + def update_order_in_db(self, data): + """Обновление заявки в БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + # Обновление основной информации о заявке + cursor.execute(""" + UPDATE orders SET + partner_id = ?, status = ?, order_date = ?, expected_production_date = ?, + delivery_method = ?, delivery_address = ?, notes = ?, total_amount = ?, final_amount = ? + WHERE order_id = ? + """, ( + data["partner_id"], + data["status"], + data["order_date"], + data["expected_production_date"], + data["delivery_method"], + data["delivery_address"], + data["notes"], + data["total_amount"], + data["total_amount"], + data["order_id"] + )) + + # Удаляем старые позиции и добавляем новые + cursor.execute("DELETE FROM order_items WHERE order_id = ?", (data["order_id"],)) + + for item in data["order_items"]: + cursor.execute(""" + INSERT INTO order_items + (order_id, product_id, quantity, unit_price, total_price) + VALUES (?, ?, ?, ?, ?) + """, ( + data["order_id"], + item["product_id"], + item["quantity"], + item["unit_price"], + item["total_price"] + )) + + conn.commit() + QMessageBox.information(self, "Успех", "Заявка обновлена.") + self.load_orders() + + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось обновить заявку: {e}") + finally: + conn.close() + + # === Методы для продукции (заглушки) === + def add_product(self): + QMessageBox.information(self, "Информация", "Добавление продукции будет реализовано в полной версии.") + + def edit_product(self): + QMessageBox.information(self, "Информация", "Редактирование продукции будет реализовано в полной версии.") + + def view_product_materials(self): + QMessageBox.information(self, "Информация", "Просмотр состава продукции будет реализовано в полной версии.") + + def delete_product(self): + product_id = self.get_selected_product_id() + if not product_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить этот продукт?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("UPDATE products SET is_active = 0 WHERE product_id = ?", (product_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Продукт удален.") + self.load_products() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить продукт: {e}") + finally: + conn.close() + + # === Методы для сотрудников (заглушки) === + def add_employee(self): + QMessageBox.information(self, "Информация", "Добавление сотрудника будет реализовано в полной версии.") + + def edit_employee(self): + QMessageBox.information(self, "Информация", "Редактирование сотрудника будет реализовано в полной версии.") + + def view_equipment_access(self): + QMessageBox.information(self, "Информация", "Просмотр доступа к оборудованию будет реализовано в полной версии.") + + def delete_employee(self): + employee_id = self.get_selected_employee_id() + if not employee_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить этого сотрудника?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("UPDATE employees SET is_active = 0 WHERE employee_id = ?", (employee_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Сотрудник удален.") + self.load_employees() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить сотрудника: {e}") + finally: + conn.close() + + # === Методы для материалов (заглушки) === + def add_material(self): + QMessageBox.information(self, "Информация", "Добавление материала будет реализовано в полной версии.") + + def edit_material(self): + QMessageBox.information(self, "Информация", "Редактирование материала будет реализовано в полной версии.") + + def view_stock_history(self): + QMessageBox.information(self, "Информация", "Просмотр истории запасов будет реализовано в полной версии.") + + def delete_material(self): + material_id = self.get_selected_material_id() + if not material_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить этот материал?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("DELETE FROM materials WHERE material_id = ?", (material_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Материал удален.") + self.load_materials() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить материал: {e}") + finally: + conn.close() + + +# === Точка входа === +if __name__ == "__main__": + # Инициализация базы данных + try: + init_database() + print("✅ База данных успешно инициализирована") + except Exception as e: + print(f"❌ Ошибка инициализации БД: {e}") + sys.exit(1) + + # Создание приложения + app = QApplication(sys.argv) + app.setFont(QFont(APP_STYLES['font_family'], 10)) + + # Авторизация + auth_dialog = AuthDialog() + if auth_dialog.exec() != QDialog.DialogCode.Accepted: + sys.exit(0) + + # Создание главного окна + main_window = MainWindow(auth_dialog.user_id, auth_dialog.user_name, auth_dialog.user_role) + main_window.show() + + sys.exit(app.exec()) diff --git a/masterpol.db b/masterpol.db index 5cb6c5fde59e6663075d00c6cea3faf6e1bc79bc..6ff4e21aec327fc63e66188c2a7534c90f00215e 100644 GIT binary patch delta 776 zcmZp8z}oPDb%Hdb!bBNoMum+D3;Fr^su);!_A>D7_FUT!oKLu+tZK|Sxm~WGk!7=-{4)UwE`D!j{tEsl{8#yp@$cYY$luN%!|%O;)+xv^oG>ylGSM|K)-^C>)M0p) zV8stq;LiwFP_|w%Bmg3Bq-$ir@`$l*fjJ*g-V-c;VfKX$7xrA(abfR;4HugfoO2S3 zi&OIy0*X>|GfQ(*AQ}yIjSW~=F_~pp@PcdxYGh&p*(_xQGuY68QO9bAB@fhIpfu3l zKuMT(BU6?~RYGRm91NUbYw_DE2RGG}brr+PH|AVGePCNbmP*1MWdyXe)WDJxC=2#1 z$WCdflMKOrW%#0E#Q}0MzQAIKIvN<7SObdkv4U*I9at<-dvOF7Gns+Kgg3Al PkpoKrJ+PJ;FggGL3|7vE delta 136 zcmZp8z}oPDb%Hdb)I=F)MyZVn3;FpO7#LW1_A>D7<*VYY$F(W=g>#_W^(EBK%AU*$i>zk`1ve>;B+zxT#Q gL;lI$_6ocLK!f36bA^4+gT*WX0^3;(82{J<00FWn2mk;8 diff --git a/ressult/.env b/ressult/.env new file mode 100644 index 0000000..5a2d5c7 --- /dev/null +++ b/ressult/.env @@ -0,0 +1,6 @@ +# .env +DATABASE_URL=postgresql://postgres:213k2010###@localhost/masterpol +SECRET_KEY=your-secret-key-here +DEBUG=True +HOST=0.0.0.0 +PORT=8000 diff --git a/ressult/app/__pycache__/database.cpython-314.pyc b/ressult/app/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a03691d1c4db33d8e2a1c1949623f88532f58fdd GIT binary patch literal 3650 zcmaJ^|8o<^72o?N$&xJji)^dNvLx&Zln@)R9h%rg0ow^oiBW}1ryb*)Lps}P&N^l9 z)Zm{m&`!*Ri34HAX{J+5e@KS>QYQmV3W=G(KX3;WX)b1lPX7TDdxlK-rEgC<$@ZA6 z?cKL;_r2ZsZr|rUzv!uTB4}?sj%GR?2>p*ttj3m!)jt9;gOW%^V<<@-rc}xtX_bbN zKFl0qRW?M{n8Vy5n`$#@>abn4pRh$xPgzX}HK7PfvMngd_0T1z>Nw%3?oexzR6B~< z^8bWYr}j5(NxQC}({AY(!rFBh=EC}nwy1rfE$L_V_rv;`Fwl$ISFrEjWXGcRt@f?{ zzW$eRLRQoXEFSyK5vO()mO()KQo8|SX9xvKfeyoTM!P}u!6Fgza%2Q%jTF7QZscTSF-d5#YS8iIJ z?8?2Pc8zBwNyJ@PRFu4=b_uy0Y%SfsGdFEGcs`TOs621fTSj$SEHCI+eqkC@+}1YB zUhozJt+VU{hu<6@I{d4|z70;W0Rx=iUVRL=n*kRj$d*k_3?kI}oV7&7Emff%WimZR zRTW7Mv7cKr%8q`_t2DW9by z(QHxGF@i==!e5Mzp)#nC38D-YWAjrWrCot|zNMej z--FnOm?j|$!3@!Q6WAYEY(Nw3rUf9dWj%ZlWFaD70$40T_yeeD-)LW#n7098&VsH* z!WuT*g9k_Z&ma)OAnJLlW6iF0w*adrH46Y?x z?SiU`Q#sY3La|gEOg%Y?^)i_ zyT&m)nm-)8e00TunzpTYP@Qk?^!d}8qwPrmwRW#;t1^CEt402Bp=Gz;vU?42_2FVm zTj`j^1OJKiK4|G(_P1Vfe&Sr{DYSR%?cERjJ&*k1$8DjH?ax+RAn=pY23P-%?LFwD z@A#SqUgGY=_6_Xh?nY_S@8p0poFa~~njHue9US0l6CEm$Xl+52^p#@*=s1&DFJInd zCAdC{r}A{3;j1$2aW+xGhpNusHCPD{B9uyydu(c!&IEZpn_6X!eEdx>;(qO~ZC<=*ZB($Z*Vq!=M|$z{Ci{E}jxod6h&nB-Wh4<#Q12hEq`mtSawj)JelJ zE~=?XL6Sg7o|?+2h6Bs*rsRB9#Vtga=WtdPF`$iT!b1sdYKBgw#GIM9VY?)q6jE=; zY$cfB-pPx2+OXvWEKDf|OMn4)fGQk*QuUOwSAGjEp4_*!~M2mIJ^PY>IkMj@eUMe>F=ld@9eYC&O z7}Xo2_ZxQ>8oTwz?!`le#{Ksi_ZI_^EAhX^?+120wWEg6r62$a=yF|jU9K&*1m{m* zJpI%K3-iJG;77q{Yi?BMFLS4IN<%Hya^Y-CK4a!Mk~-m$FTwl24sW>}2c!&#{J3@HJq*c+Ko)L-&fv_Xheeo{A#?={tEnR?XQq;cl7+xf;*zSBM;r} zPZ-qk>iIaX2PsTyDr|=?<|I{QR+ti@igfb)l$_2>gm&}rgaxU*;^q1A3|3SrlNGZv z&tn30W_04cgfj2g#Pbk}Y9_@)YQ>q85UL8PO?f^g35t@2Qa>e163;{4uU*FkF_KAM zFPTfe0VW+u#%7ZG@jWo}2b$r6KRD~S&{~I=^F7_pa2+cMx{^Ue0Bo`I@b5zk?nf%$ zQp9_$vY}HvrD8!T6$-Y}e;JqnIBq2!(O}T&lh;s5qge|$`b!RZ4G+LFxurrL#0o=E x)DxDW*i{FjoZlnwBlN-})b>4c{TI3ZT>F&WMRhzuq#LD>&2#P#XK7RG{{cp@kqH0* literal 0 HcmV?d00001 diff --git a/ressult/app/__pycache__/main.cpython-314.pyc b/ressult/app/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1c1369f03d149b3f13e6a19b00c99c731d1d9c9 GIT binary patch literal 2639 zcmc&$TWl0n7(R2^neE<6cYER9sTFV~cEE5mv>*Y@7A=%1PfFQLcBk!x*`4*wS+E-eDZCvyy%nvneBE9u1VjVWX}1|`7dYw z@B7a=vojVAA$WdzwB0-{BlJ5PRG;9ou=GBU&=}GXLFbUh^>T!Rp6}&T0ukEToX{(# z0wmzeVy~2viR{aP-e9VMG^9c#lnRq@DncTuD2b+GB-V}w{R6 zr+28X_d2aN^bc)`h^X?UTN1=0cflj-Z={@(V z@`mn^BmKP!{m~txvtYMCzo#>Fp3b`0Li8pK7u<1hopHzAOH2YkkQEq?f!%jta0_Dl z_H<66U(-p5MHdtZHV^hQ^fnkyxgW!LcGc3LxDz^=%&IQ$P;B?6qLE*=YW%TeBg}YV=)cTTvOaj00PT-hR(TH z6iA$5#BjU^Q+~!Q)hTxjY~}$KrksN;!Eu4!W|)IC&#>@OnP!1NGtYvqfC6J?1NIAw z`x(7UfAFKQ1*ZNE?C#ptwJRxj0b&x%@WNTc$zZd<^7P~jfCLk`m(L_cFRELXeKBog zbJzqBnkvH#HnNbXsasCfB4>~hJG(L*G4w11Dy>uXf~j8EtyV!UC6@7WRsojeJ*j|= zA@e;?B>J#ZidNlxMoQ5EMvRg&9b)6rQmm@25LF5v(}}M1<_b0@rRJ*g8TKMaS8-Vj zP+`-H9`#u*VxQ|$q#DAfJaz<#XW3?at;eaE>aJqF1n*^p#HEcOynx!-;mm2gCTOA- z&?HspM0*93OUlKUz<}O>(jNmFW`OYp*f&N#Td+;2nP&^blXHgSKs}XqtfA6af#Dd0 z$6?k)p(&2D)o#{`jZXa^#r7@2o8d#2;6>g6b;QmYYF5vihNT`eob$vksD)87q88pI z>X2z!239d7UbF~sP*8KaneQr$dLp)Mg4-boyE&X~(EI^FmxHJwP7~dW!KR7*i@}yM zLhH}+Wm$|!Wh4fqzZ?-P#~Z?2NmijGEi&>9cW{+fxQl~O<4%QR&gV=h?gIt=5*TAA z65EV-0Y}(P(>{$b>0X6RUGJRpB!}pv=y-Bv;ZYxmO0xx`;;qnlk%}^%88I^FF}o~Z zf!AT*<0mMgXv>62!wMCZSYilHBiLw|CFHd&kGgUzr(Rda z#ts&kR;nC)m?7hZ0OT%g7m49uc57o+%^3bY5N1CdxPdk7r^FM$(u?WYY`PZJi<$Y1 zRm>V`KLqxq9KbxivNnvE+U~4XJYSU_x0{?8fQPG zhfevr{+e#zWY6^;ntbj4mivv*>$)E#w!4Y#R6YLlIr`RFUp-jIwVl3x+E;2d<(|od z*AIU60qyCd-6wtJsYjgnN!z8iPZO6nKj9@#S`rW+D>JFQ4)FnBx8WBq@r38O4yJ7W JlS!Br*gt9!SxNu^ literal 0 HcmV?d00001 diff --git a/ressult/app/database.py b/ressult/app/database.py new file mode 100644 index 0000000..8006102 --- /dev/null +++ b/ressult/app/database.py @@ -0,0 +1,60 @@ +# app/database.py +""" +Модуль для работы с базой данных PostgreSQL +Соответствует требованиям ТЗ по разработке базы данных +""" +import os +import psycopg2 +from psycopg2.extras import RealDictCursor +from dotenv import load_dotenv +import time + +load_dotenv() + +class Database: + def __init__(self): + self.connection = None + self.max_retries = 3 + self.retry_delay = 1 + + def get_connection(self): + """Получение подключения к базе данных с повторными попытками""" + if self.connection is None or self.connection.closed: + for attempt in range(self.max_retries): + try: + self.connection = psycopg2.connect( + os.getenv('DATABASE_URL'), + cursor_factory=RealDictCursor + ) + break + except psycopg2.OperationalError as e: + if attempt < self.max_retries - 1: + time.sleep(self.retry_delay) + continue + else: + raise e + return self.connection + + def execute_query(self, query, params=None): + """Выполнение SQL запроса с обработкой ошибок""" + conn = self.get_connection() + try: + with conn.cursor() as cursor: + cursor.execute(query, params) + if query.strip().upper().startswith('SELECT'): + return cursor.fetchall() + conn.commit() + return cursor.rowcount + except psycopg2.InterfaceError: + self.connection = None + raise + except Exception as e: + conn.rollback() + raise e + + def close(self): + """Закрытие соединения с базой данных""" + if self.connection and not self.connection.closed: + self.connection.close() + +db = Database() diff --git a/ressult/app/main.py b/ressult/app/main.py new file mode 100644 index 0000000..65beb14 --- /dev/null +++ b/ressult/app/main.py @@ -0,0 +1,48 @@ +# app/main.py +""" +Главный модуль FastAPI приложения +Соответствует требованиям ТЗ по интеграции модулей +""" +import os +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from dotenv import load_dotenv +from app.routes import partners, sales, upload, calculations, auth, config + +load_dotenv() + +app = FastAPI( + title="MasterPol Partner Management System", + description="REST API для системы управления партнерами согласно ТЗ демонстрационного экзамена", + version="1.0.0" +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Регистрация маршрутов согласно модулям ТЗ +app.include_router(partners.router, prefix="/api/v1/partners", tags=["Partners Management"]) +app.include_router(sales.router, prefix="/api/v1/sales", tags=["Sales History"]) +app.include_router(upload.router, prefix="/api/v1/upload", tags=["Data Import"]) +app.include_router(calculations.router, prefix="/api/v1/calculations", tags=["Calculations"]) +app.include_router(config.router, prefix="/api/v1/config", tags=["Configuration"]) +app.include_router(auth.router, prefix="/api/v1/auth", tags=["Authentication"]) + +@app.get("/") +async def root(): + """Корневой endpoint системы""" + return { + "message": "MasterPol Partner Management System API", + "version": "1.0.0", + "description": "Система управления партнерами согласно ТЗ демонстрационного экзамена" + } + +@app.get("/health") +async def health_check(): + """Проверка здоровья приложения""" + return {"status": "healthy"} diff --git a/ressult/app/models/__init__.py b/ressult/app/models/__init__.py new file mode 100644 index 0000000..10fccba --- /dev/null +++ b/ressult/app/models/__init__.py @@ -0,0 +1,75 @@ +# app/models/__init__.py +""" +Модели данных Pydantic для валидации API запросов и ответов +Соответствует ТЗ демонстрационного экзамена +""" +from pydantic import BaseModel, EmailStr, validator, conint +from typing import Optional +from decimal import Decimal + +class PartnerBase(BaseModel): + partner_type: Optional[str] = None + company_name: str + legal_address: Optional[str] = None + inn: str + director_name: Optional[str] = None + phone: Optional[str] = None + email: Optional[EmailStr] = None + rating: conint(ge=0) # Рейтинг должен быть целым неотрицательным числом + sales_locations: Optional[str] = None + + @validator('phone') + def validate_phone(cls, v): + if v and not v.startswith('+'): + raise ValueError('Телефон должен начинаться с +') + return v + +class PartnerCreate(PartnerBase): + pass + +class PartnerUpdate(PartnerBase): + pass + +class Partner(PartnerBase): + partner_id: int + + class Config: + from_attributes = True + +class SaleBase(BaseModel): + partner_id: int + product_name: str + quantity: Decimal + sale_date: str + +class SaleCreate(SaleBase): + pass + +class Sale(SaleBase): + sale_id: int + + class Config: + from_attributes = True + +class UploadResponse(BaseModel): + message: str + processed_rows: int + errors: list[str] = [] + +class MaterialCalculationRequest(BaseModel): + product_type_id: int + material_type_id: int + quantity: conint(ge=1) + param1: float + param2: float + product_coeff: float + defect_percent: float + +class MaterialCalculationResponse(BaseModel): + material_quantity: int + status: str + +class DiscountResponse(BaseModel): + partner_id: int + total_sales: Decimal + discount_percent: int diff --git a/ressult/app/models/__pycache__/__init__.cpython-314.pyc b/ressult/app/models/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4c160bf4a63727ac4ff95d53ba806c9d047d7b4 GIT binary patch literal 6082 zcmb_g-ES1v6~D7H`{`Y;zt*3CmyepbfK38Tsh}x|p)%z&OQ324OsBhJm}b1Y+!+U1 z>cg7Q1gW5yN+?Cj1MM4cQCixx3GTnJTU3=!TZyO-Ro}3^Dt+wl+?iQs&Bk`4u03b& zoO|ZpbI-@`+&eqkQwfIOzQ-em9%bww^x*%(USoNu#8`#pn9VM*oODE*4B4Sc*_J02 zTbT^o;bG#3jzlJ-c2uHwwdLWK9&og5ABuR@$7`)M!=1pPc*n~f*S)j z9^fVgHvw*2fSVHBB)F--xb1@54sQBMyGw&;<9ZpF!pU^8r!Xm*J^k$kCW6zx&TiJdIlMyaToXh#p}d1G2L z^L_}?-E%uz{sA5|1k!nCO9-aWg@i5lvJyhdR&a%JMQ}xN#c&}E5Imv12tNAd2h6~W4M_Mp_}7}bn7j!!&h1&UMS0G3{}! zTpph$*IMJMY7`AyRmaM+PP^w_T@@yC1_O2+YwpWnT5B6fh5fTFIsf_x=Wd<5yY
>Cyk&@e6&MeyOo?AkW*HX3$M}_3G{_l=n?h$Q01fwo$z3q%F<;GI3Uch!`UMVU#`=Uthal35I+xfo$R8}3Zp{$@?tz8ED&j3rWYlZ$a`Cjy;q z#7MGa&#j4@kRD!45u=?LlYeU4SW~twromb2V5#nT?Z&a1(!bbAB3-O^)1Cb7^EG93 zsT&O51GVn4(X$Zm%2{~*>W#f0el{^W#F)q3n5}VZKQl&FxR3%R( z{vu7ZVsa`RoX93>yhMazLa>_VC2!Ji`5;JBvD)kwD^c|&RQ=Fiucp|FC`KqxC`RF& zVn9@T?wbWpyV##Y@wH zKx^d&C*qF8_hIU7xR$Z&gC`yhZn}|nZL%MXCb%Y>$RtWeT@9{%c!ugBPO;=9${xTO z1t1hK%tp@^^tjPZ7W80-lQK?^zurJ9shiPbr9oU|>FAz2xkRAis_VvKtd&JLErstO zA|i4ZwZ=e#Y4$QPiO@7614NoVjY#3)MF#SeK+4Y@5^VCN7X)mXW@rkS?QXz`;6T72 zIFLzp7{L+oyc;_Wl%{P;Yjb}GBd*8XE2?1#Zg{Tx!6jkg68;KCMd9H9KR~@GH8G$c zcBA&J6Q{bNQZkLo{2(MbS%p)KtnRpszm&0{=Lj3DJs{6K*PV|Vri`A@;}W%8eP&MS7S z0uQL7xUd|Z#+FjUez6@mkVjiDsJwK=aw0k%Ff1g$$oqrPz)?)YS_8I+nwQgr0fd+q zrK*bo8Z}d8DQcE)fm{a(}cnvcHJ|*FyI1k_-`{Lbt0zg*}O| zfuYK`n_!)!T08hTSl_=UtPdWi&4{6y6F3Ram|_Q>(=X5HmfZyOvQRhm?~8M-Ts?LnXjJ*ihR)$WYrqxKKMr#S>u>cf%s>R|PtbAo-jQ91X-u!~uoU zaU)*flORq6rCpnT(YwFor2LojC4FkjX)owgII5^+o#%CweXpF*nw{_zf`5&W+Wy5x zYzi*|9qR&!8(x`yIt7R$NAEyQ84&Tc6k+Mi&!rnC;>)iysjj)>{*gnpz0*by-s<;H z8ks>1u|$WAKt%GRnC&^}P>D%IpyM%m63{`cbw<{qH6v@NrBPPH5Gp1UFTxi&;A=SgCZOL3l(oH3;+NC literal 0 HcmV?d00001 diff --git a/ressult/app/routes/__init__.py b/ressult/app/routes/__init__.py new file mode 100644 index 0000000..23f8410 --- /dev/null +++ b/ressult/app/routes/__init__.py @@ -0,0 +1,5 @@ +# app/routes/__init__.py +""" +Инициализация маршрутов API +""" +from . import partners, sales, upload, calculations, auth, config diff --git a/ressult/app/routes/__pycache__/__init__.cpython-314.pyc b/ressult/app/routes/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ab0a5d1ba4fe6f0a2f5d6326b50f73ef77a9f02 GIT binary patch literal 368 zcmXwzze)o^5XN`!UqUpNwgvhWVEzjghy4*%u-nZ>3vQ#3$;PVfNt zaqNK?`{2g`1aSyq96=N}ps|Jq-a6{~<&IlunLG1p=GB#%o0)lbd@{2-RW}{;;_S4# zcDk%C%$s@W^p5+jZe*J>QBW|YZKxB@w2ey1MUvU&G~sE<6UarOZIF~OvQa9EVSakl zZQ2HL-JK)k>d(aN{Ftw;*`+gdqSO+-j!=#+1G%VcRgG!>Ir+hy;$Ze zkStW7Y%Ggc&y2z;HEY%$CHp-)IZ&~NASQF$P$F2^2YNy03umG@QXGYOsd%w?RXo($ zE;^IWt>QHiB*1fSIzPZVRlMv>Io|>AWB5)56D$dUuADE}nn@zbuCC6u^BK*s^?Z&9 zM>IprsaAp^0V+AHSbBz(t&**#rfNA`SF%+WC{pvmLcM-tGUbFD1T6m~V`YSe!&XNrb*vh>VX3@0duBuIki{}&OZNoO;2NcX6H%>;J4A zF}MSqj&;<`RwErZvN}#g3ldm4*PoYHH0i#l2?ul1`PR8v9C0Rwv&Y-sZENik2b7%B zrC1dM@8nC+L58CukAsB}@wdV$vtgedd6Nw=q3!=SXr89X|BfP5Fik>Br zXAlhg3U=?VCK6mjOZ*;M5?F;tA~cU^5y(ISsa^UK+}n`|nKV{OL_4o#AluSs3z|7( z(ws7BB_N!tXKccs&X_}nO@vIpmN{dbBYqD9;kBHLF0fXOSj#pOK9dG5;US?RpNX&n zkGhnHV&VQhf;GzrTH8e!CJk-kbE}fM4QQI-);uhaAyF6j&Ayh0C7WhTYVVcQ&V`5P zBAaI;wZ%xS6W+GyV@rdh{L>(cl|L-4xzXx09lKxJ;eB62bsKT6u$=Y@Nd29K9cHcKpqMFR;M=$D%=c5x4Hfmk&3#3h2(8l)kes0`y%W z)VhnkTUy?_gT1@MN9kQGNQVV!P&4)3p$yor4q9j23P+P^xx&d`ZCI8L3%n1;PdEm1PkDr2FQUd@P|MG#@+TDeHBfrB za)f^nD!bY|;-AODmC6g1qaRM=@&^sgQ_1Ouwm)b$T4WJZvA_Ve(1aNOmH37DXger9 PVHmu{75+{s_jmpWs8&$x literal 0 HcmV?d00001 diff --git a/ressult/app/routes/__pycache__/calculations.cpython-314.pyc b/ressult/app/routes/__pycache__/calculations.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf8e18a89d63a99a4e10b0134c2d1660cd2acc0f GIT binary patch literal 2150 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 N4CKz~?+)qs^*3GqQnvsA literal 0 HcmV?d00001 diff --git a/ressult/app/routes/__pycache__/partners.cpython-314.pyc b/ressult/app/routes/__pycache__/partners.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a432e264480d068cb83d3696c1c585503b0347f GIT binary patch literal 8301 zcmcgx>vI&>mA~CR?}wfsp_kPlzzh}=M25&9jHN+XDnQ6BpD%Xn7?4V=7sXA$$8*~l0Rri2L^$d7b?|@JBQMqH#KM+s@t>jsDD|s}p zRt-KI9BwfuE8GR=qc9&{f~&sJyYfqVyRF`ydLnr1NutV{p$f*d8M^GHZ3Z8V@fVG$ zfiVx;dmDA3Fi!x+1dGOOf-#S5IHm%|R2Ge?`;z}t^{h{*{wABBwFbu37LBVHHnow^ z=FBAY@M-_1&1I*u)9|~Ty_9{0?;RfGwd>l=><{?tW$pd!ls2cmt=-gaXzywBAk^Nc z11`bi4cd51oA+tI)NX6HvzMUf4bXBCYcLw>ymkwkuEPUp)Nb+n_H^;uue6`}LagqE zxr9_k6=gj*FgiNi|6)u`t5PZ%VstkCt&l@^4@YG+iS7AHSQewIXg!`z$MLc1sHDVF znWQR+N;;KPL{Q*9D#oM>(S*))#_S90hKI|9IM%)cHnNShf?YO|ajVC0e%&kqRJMZ6 znl_Rq9VE?+kt%ZRX+qNMxM|~LjJ5i3<2Vbw#<*dxSuflLtM547J=pSSerD5j;&p^H zlkuQAYK#-u_D1sw`sZyNr4c%CuFNQ^`daVPppFvKZP z_!cOC8;lsu_f=29m-P>s|{2bWTxa znFDnpC%mAjWD=?#G+!%{fOpY3IrV}bFh2yHg%K6pkaS`7aOe5d1+g<8O-f>-^QfqN zTTP`q(-Z3X&h&F?=UFL{5amw%E}*;Yi|n_6eNpFcKz;$CIL zO#4dX?rh`ktKCbDk6ss7DtkVx?716izdAm@>!Us4rO?sW0yF=Zt=hR7XjlpG*#JM= z{BfY;Ge6nX2s*1;G*9#A_xzjI3Fqut_3yhrGGG19*s}l7YVGEK9lTr7bd|dvo@d@U zwp?*Y^BnqX&JF$krW}G7zQpt$W?t^??mbKvda%B5*xz@MTl7@-?dKL77_2usLB7~p zhm!qHlpN$hG8ydrXLF^c_)9SaAN=8kz=dCGZ95cGWQ?3)yI% iA6ih<3~6acmp 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 literal 0 HcmV?d00001 diff --git a/ressult/app/routes/__pycache__/sales.cpython-314.pyc b/ressult/app/routes/__pycache__/sales.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77df468c57e3a526a47cdbab9056da516b89157b GIT binary patch literal 4390 zcmb`KU2Ggz6@c%Znf+h?#&vA39glyKX2r%Elr*7^!EBvP?Gn~z>=-IJS#5SF&5E^m zGBXRcTO>PisjI3j52yqwr3t(c!Gmk!#x#z}J|XddW|N|zQ4vKvqz)q{=Z}o_j2)#8iDoR)a4TglIDHgE+py!doO9$mlnly->}QNpgn! z$|4~NXlp0mqUWhZr=Z6wK?*0yJ-{)hyKM0~HTmR>4hylLORLi{T|?{L=GTMg)6;S= z5t~WMso-&0`>vi=g37FZKB!#KgXfZ|6f8}ZH7%3UgE2)3s<=TdsKFMrK4sSQMx(Kr znY0d@iAL30uv!C;whO9x@)4<}0}rwK^&$4N3C)D<4h1X@crFnlZ2l31Zqf;(e5Aae zG2F?v(gnm!l0)=xp--Wcw4R*eGF%_Ya3^353O8jd4w~eOo)_W1QBR1A>;Wx>>w&tH zNl7A-DSJ`hP7>{SvI;*1^jbyYraVP?f-l;W_@jjtz&j8WevnVnBss=+kR*KqWWO!* z8jNxqZ-cYl0&jlV@>yf`e{?c*P-NpGxa}%9=7zDpldA&?fCc#It=#M4VdmoQxfQoq zQO3h3!$T7yzD@AEfLQhzkkxb|6W60Nv1vIVUdY5|^rSu;u#^+ z{oBJ~DO_P!{HnNjo64wkJS>S%JzKUc9uJQXmCQWY0%yI>nH~+iETcu^>4a?B6S5vl zrXttvssM*<*2vGxad6G(g^aAunnEHO*G*RmK@|xyc}-XSf{O3ew1ZcJQ=7b+{=P|N z4Xx~hqfya+&8FvMJ<6OsdUiHiI2N@7y1ejc38)@`tLD8f+rS46yY4r1-EHW~6I+ir z?zjCfLv`lD%{`($h+8Z(>6fXJ)v$s z>u3&j@YxO<%H2G4nKT%OaivmX><`fN`57zvxdL|%@I@>5q7{76qFxnWa1Ryu0wx~G z7xyWAm3ZC87dOa#g0Jtuh8~62N^SuNd8sNkh`9ye&bXahWS9GT;Wl42ek_|D^FU@oZ0{6zL-6ZzL8UUpB{{l!4U2N+oKWlnISmFA~4I1j9 ztG0gfo+l*oSzB|+$7g*ultmu8zG!?^ov<%cKu_>f6+;L7u#16(RdyyU7P9I9o$iMqt-#^qXVK7Y<+%MC&?JzVA~3`mI7P33tDjl+X}R@#I?HlkWdb@5;sZD zkN`SDp-5ciKTJLVbvy`GN}-gS!fmo5@l)tptW1;Z%i}hXVMxyaV#wYgYhysO2x+B-t8+iC0$?OE9+oD=k;ueoA!zNdV!4Xk3Pv$b zBBA~&Ym=dqr^4gn{=HfNf?M&a){(*!r=-Y8ERk$SlA*eyv7a4CeiMfMHzb!++fMixl=uQyCB92s z(J2h=tVlvRoKBQNvSh2^%C68sx-En{GC>$_{DLu6_@+Xxc8006nJ^hS+oRTrOt<(*XnD?qm*yQKHq|GY5X2-*y13h;lRc` cO8FM->{#endhs4@-{OE0?CkglN^HIV0afX75dZ)H literal 0 HcmV?d00001 diff --git a/ressult/app/routes/__pycache__/upload.cpython-314.pyc b/ressult/app/routes/__pycache__/upload.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a09ba3334c3e8d1280222a4f701b92f4a8290d5b GIT binary patch literal 5317 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 literal 0 HcmV?d00001 diff --git a/ressult/app/routes/auth.py b/ressult/app/routes/auth.py new file mode 100644 index 0000000..acae8ac --- /dev/null +++ b/ressult/app/routes/auth.py @@ -0,0 +1,45 @@ +# app/routes/auth.py +""" +Маршруты API для аутентификации +""" +from fastapi import APIRouter, HTTPException, Depends +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from app.database import db +import bcrypt + +router = APIRouter() +security = HTTPBasic() + +@router.post("/login") +async def login(credentials: HTTPBasicCredentials = Depends(security)): + """Аутентификация менеджера""" + try: + result = db.execute_query( + "SELECT manager_id, username, password_hash, full_name FROM managers WHERE username = %s AND is_active = TRUE", + (credentials.username,) + ) + + if not result: + raise HTTPException(status_code=401, detail="Invalid credentials") + + manager = dict(result[0]) + stored_hash = manager['password_hash'] + + # Проверка пароля + if bcrypt.checkpw(credentials.password.encode('utf-8'), stored_hash.encode('utf-8')): + return { + "manager_id": manager['manager_id'], + "username": manager['username'], + "full_name": manager['full_name'], + "authenticated": True + } + else: + raise HTTPException(status_code=401, detail="Invalid credentials") + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/verify") +async def verify_token(): + """Проверка валидности токена""" + return {"verified": True} diff --git a/ressult/app/routes/calculations.py b/ressult/app/routes/calculations.py new file mode 100644 index 0000000..c41200b --- /dev/null +++ b/ressult/app/routes/calculations.py @@ -0,0 +1,43 @@ +# app/routes/calculations.py +""" +Маршруты API для расчетов +Соответствует модулю 4 ТЗ по расчету материалов +""" +from fastapi import APIRouter, HTTPException +from app.models import MaterialCalculationRequest, MaterialCalculationResponse +import math + +router = APIRouter() + +@router.post("/calculate-material", response_model=MaterialCalculationResponse) +async def calculate_material(request: MaterialCalculationRequest): + """ + Расчет количества материала для производства продукции + Соответствует модулю 4 ТЗ + """ + try: + # Валидация входных параметров + if (request.param1 <= 0 or request.param2 <= 0 or + request.product_coeff <= 0 or request.defect_percent < 0): + return MaterialCalculationResponse( + material_quantity=-1, + status="error: invalid parameters" + ) + + # Расчет количества материала на одну единицу продукции + material_per_unit = request.param1 * request.param2 * request.product_coeff + + # Расчет общего количества материала с учетом брака + total_material = material_per_unit * request.quantity + total_material_with_defect = total_material * (1 + request.defect_percent / 100) + + # Округление до целого числа в большую сторону + material_quantity = math.ceil(total_material_with_defect) + + return MaterialCalculationResponse( + material_quantity=material_quantity, + status="success" + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ressult/app/routes/config.py b/ressult/app/routes/config.py new file mode 100644 index 0000000..10e68ea --- /dev/null +++ b/ressult/app/routes/config.py @@ -0,0 +1,32 @@ +# app/routes/config.py +""" +Маршруты API для управления конфигурацией +""" +from fastapi import APIRouter, HTTPException +from pathlib import Path +import json + +router = APIRouter() + +CONFIG_PATH = Path(__file__).parent.parent.parent / "config.json" + +@router.get("/") +async def get_config(): + """Получение текущей конфигурации""" + try: + if CONFIG_PATH.exists(): + with open(CONFIG_PATH, 'r', encoding='utf-8') as f: + return json.load(f) + return {"message": "Config file not found"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error reading config: {str(e)}") + +@router.put("/") +async def update_config(config_data: dict): + """Обновление конфигурации""" + try: + with open(CONFIG_PATH, 'w', encoding='utf-8') as f: + json.dump(config_data, f, indent=4, ensure_ascii=False) + return {"message": "Configuration updated successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error saving config: {str(e)}") diff --git a/ressult/app/routes/partners.py b/ressult/app/routes/partners.py new file mode 100644 index 0000000..8c64889 --- /dev/null +++ b/ressult/app/routes/partners.py @@ -0,0 +1,157 @@ +# app/routes/partners.py +""" +Маршруты API для управления партнерами +Соответствует модулям 1-3 ТЗ +""" +from fastapi import APIRouter, HTTPException +from app.database import db +from app.models import Partner, PartnerCreate, PartnerUpdate, DiscountResponse +from decimal import Decimal + +router = APIRouter() + +@router.get("/") +async def get_partners(): + """ + Получение списка всех партнеров + Соответствует требованию просмотра списка партнеров + """ + try: + result = db.execute_query(""" + SELECT partner_id, partner_type, company_name, legal_address, + inn, director_name, phone, email, rating, sales_locations + FROM partners + ORDER BY company_name + """) + + partners_list = [] + for row in result: + partner_dict = dict(row) + # Преобразуем рейтинг к int если нужно + if isinstance(partner_dict.get('rating'), float): + partner_dict['rating'] = int(partner_dict['rating']) + partners_list.append(partner_dict) + + return partners_list + + except Exception as e: + if "relation \"partners\" does not exist" in str(e): + return [] + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/{partner_id}") +async def get_partner(partner_id: int): + """Получение информации о конкретном партнере""" + try: + result = db.execute_query( + "SELECT * FROM partners WHERE partner_id = %s", + (partner_id,) + ) + if not result: + raise HTTPException(status_code=404, detail="Partner not found") + + partner_data = dict(result[0]) + # Преобразуем рейтинг к int если нужно + if isinstance(partner_data.get('rating'), float): + partner_data['rating'] = int(partner_data['rating']) + + return partner_data + + except Exception as e: + if "relation \"partners\" does not exist" in str(e): + raise HTTPException(status_code=404, detail="Partner not found") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/") +async def create_partner(partner: PartnerCreate): + """ + Создание нового партнера + Включает валидацию данных согласно ТЗ + """ + try: + result = db.execute_query(""" + INSERT INTO partners + (partner_type, company_name, legal_address, inn, director_name, + phone, email, rating, sales_locations) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING partner_id + """, ( + partner.partner_type, partner.company_name, partner.legal_address, + partner.inn, partner.director_name, partner.phone, partner.email, + partner.rating, partner.sales_locations + )) + return {"partner_id": result[0]["partner_id"]} + except Exception as e: + if "duplicate key value violates unique constraint" in str(e): + raise HTTPException(status_code=400, detail="Partner with this INN already exists") + raise HTTPException(status_code=500, detail=str(e)) + +@router.put("/{partner_id}") +async def update_partner(partner_id: int, partner: PartnerUpdate): + """ + Обновление данных партнера + Соответствует требованию редактирования данных партнера + """ + try: + db.execute_query(""" + UPDATE partners SET + partner_type = %s, company_name = %s, legal_address = %s, + inn = %s, director_name = %s, phone = %s, email = %s, + rating = %s, sales_locations = %s + WHERE partner_id = %s + """, ( + partner.partner_type, partner.company_name, partner.legal_address, + partner.inn, partner.director_name, partner.phone, partner.email, + partner.rating, partner.sales_locations, partner_id + )) + return {"message": "Partner updated successfully"} + except Exception as e: + if "duplicate key value violates unique constraint" in str(e): + raise HTTPException(status_code=400, detail="Partner with this INN already exists") + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete("/{partner_id}") +async def delete_partner(partner_id: int): + """Удаление партнера""" + try: + db.execute_query( + "DELETE FROM partners WHERE partner_id = %s", + (partner_id,) + ) + return {"message": "Partner deleted successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/{partner_id}/discount", response_model=DiscountResponse) +async def calculate_partner_discount(partner_id: int): + """ + Расчет скидки для партнера на основе общего количества продаж + Соответствует модулю 2 ТЗ + """ + try: + # Получаем общее количество продаж партнера + result = db.execute_query(""" + SELECT COALESCE(SUM(quantity), 0) as total_sales + FROM sales WHERE partner_id = %s + """, (partner_id,)) + + total_sales = result[0]["total_sales"] if result else Decimal('0') + + # Расчет скидки согласно бизнес-правилам ТЗ + if total_sales < 10000: + discount = 0 + elif total_sales < 50000: + discount = 5 + elif total_sales < 300000: + discount = 10 + else: + discount = 15 + + return DiscountResponse( + partner_id=partner_id, + total_sales=total_sales, + discount_percent=discount + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ressult/app/routes/sales.py b/ressult/app/routes/sales.py new file mode 100644 index 0000000..fac35e0 --- /dev/null +++ b/ressult/app/routes/sales.py @@ -0,0 +1,64 @@ +# app/routes/sales.py +""" +Маршруты API для управления продажами +Соответствует требованиям ТЗ по истории реализации продукции +""" +from fastapi import APIRouter, HTTPException +from app.database import db +from app.models import Sale, SaleCreate + +router = APIRouter() + +@router.get("/partner/{partner_id}") +async def get_sales_by_partner(partner_id: int): + """ + Получение истории реализации продукции партнером + Соответствует модулю 4 ТЗ + """ + try: + result = db.execute_query(""" + SELECT sale_id, partner_id, product_name, quantity, sale_date + FROM sales + WHERE partner_id = %s + ORDER BY sale_date DESC + """, (partner_id,)) + return [dict(row) for row in result] + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/") +async def get_all_sales(): + """Получение всех продаж с информацией о партнерах""" + try: + result = db.execute_query(""" + SELECT s.sale_id, s.partner_id, p.company_name, s.product_name, + s.quantity, s.sale_date + FROM sales s + JOIN partners p ON s.partner_id = p.partner_id + ORDER BY s.sale_date DESC + """) + return [dict(row) for row in result] + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/") +async def create_sale(sale: SaleCreate): + """Создание новой записи о продаже""" + try: + result = db.execute_query(""" + INSERT INTO sales (partner_id, product_name, quantity, sale_date) + VALUES (%s, %s, %s, %s) + RETURNING sale_id + """, (sale.partner_id, sale.product_name, sale.quantity, sale.sale_date)) + return {"sale_id": result[0]["sale_id"]} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete("/{sale_id}") +async def delete_sale(sale_id: int): + """Удаление записи о продаже""" + try: + db.execute_query("DELETE FROM sales WHERE sale_id = %s", (sale_id,)) + return {"message": "Sale deleted successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ressult/app/routes/upload.py b/ressult/app/routes/upload.py new file mode 100644 index 0000000..5e4339d --- /dev/null +++ b/ressult/app/routes/upload.py @@ -0,0 +1,103 @@ +# app/routes/upload.py +""" +Маршруты API для загрузки и импорта данных +Соответствует требованиям ТЗ по импорту данных +""" +import pandas as pd +from fastapi import APIRouter, UploadFile, File, HTTPException +from app.database import db +from app.models import UploadResponse + +router = APIRouter() + +@router.post("/partners") +async def upload_partners(file: UploadFile = File(...)): + """ + Загрузка партнеров из файла + Подготовка данных для импорта согласно ТЗ + """ + try: + if file.filename.endswith('.xlsx'): + df = pd.read_excel(file.file) + elif file.filename.endswith('.csv'): + df = pd.read_csv(file.file) + else: + raise HTTPException(status_code=400, detail="Unsupported file format") + + processed = 0 + errors = [] + + for index, row in df.iterrows(): + try: + # Валидация и преобразование данных + rating = row.get('rating', 0) + if pd.isna(rating): + rating = 0 + + db.execute_query(""" + INSERT INTO partners + (partner_type, company_name, legal_address, inn, director_name, + phone, email, rating, sales_locations) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + row.get('partner_type'), + row.get('company_name'), + row.get('legal_address'), + row.get('inn'), + row.get('director_name'), + row.get('phone'), + row.get('email'), + int(rating), # Конвертация в целое число + row.get('sales_locations') + )) + processed += 1 + except Exception as e: + errors.append(f"Row {index}: {str(e)}") + + return UploadResponse( + message="File processed successfully", + processed_rows=processed, + errors=errors + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/sales") +async def upload_sales(file: UploadFile = File(...)): + """Загрузка продаж из файла""" + try: + if file.filename.endswith('.xlsx'): + df = pd.read_excel(file.file) + elif file.filename.endswith('.csv'): + df = pd.read_csv(file.file) + else: + raise HTTPException(status_code=400, detail="Unsupported file format") + + processed = 0 + errors = [] + + for index, row in df.iterrows(): + try: + db.execute_query(""" + INSERT INTO sales + (partner_id, product_name, quantity, sale_date) + VALUES (%s, %s, %s, %s) + """, ( + int(row.get('partner_id')), + row.get('product_name'), + row.get('quantity'), + row.get('sale_date') + )) + processed += 1 + except Exception as e: + errors.append(f"Row {index}: {str(e)}") + + return UploadResponse( + message="File processed successfully", + processed_rows=processed, + errors=errors + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ressult/config.json b/ressult/config.json new file mode 100644 index 0000000..a73381d --- /dev/null +++ b/ressult/config.json @@ -0,0 +1,24 @@ +{ + "application": { + "name": "MasterPol Partner Management System", + "version": "1.0.0", + "company_logo": "resources/logo.png", + "app_icon": "resources/icon.png" + }, + "api": { + "base_url": "http://localhost:8000", + "timeout": 30 + }, + "style": { + "primary_color": "#007acc", + "secondary_color": "#005a9e", + "accent_color": "#28a745", + "font_family": "Arial", + "font_size": "12px" + }, + "features": { + "enable_import": true, + "enable_export": true, + "enable_calculations": true + } +} diff --git a/ressult/database_init.py b/ressult/database_init.py new file mode 100644 index 0000000..a0ac362 --- /dev/null +++ b/ressult/database_init.py @@ -0,0 +1,196 @@ +# database_init.py +""" +Скрипт инициализации базы данных с исправлением ошибки типа данных +""" +import argparse +import sys +import os +from app.database import db +import bcrypt + +def parse_arguments(): + """Парсинг аргументов командной строки""" + parser = argparse.ArgumentParser(description='Инициализация базы данных MasterPol') + parser.add_argument('--host', default='localhost', help='Хост PostgreSQL') + parser.add_argument('--port', default='5432', help='Порт PostgreSQL') + parser.add_argument('--database', default='masterpol', help='Имя базы данных') + parser.add_argument('--username', default='postgres', help='Имя пользователя PostgreSQL') + parser.add_argument('--password', required=True, help='Пароль пользователя PostgreSQL') + + return parser.parse_args() + +def initialize_database(db_url): + """Инициализация структуры базы данных с тестовыми данными""" + + # Устанавливаем URL базы данных + os.environ['DATABASE_URL'] = db_url + + # Удаляем существующие таблицы (для чистой инициализации) + drop_tables = """ + DROP TABLE IF EXISTS sales CASCADE; + DROP TABLE IF EXISTS partners CASCADE; + DROP TABLE IF EXISTS managers CASCADE; + """ + + # Создание таблицы партнеров с правильным типом для rating + partners_table = """ + CREATE TABLE IF NOT EXISTS partners ( + partner_id SERIAL PRIMARY KEY, + partner_type VARCHAR(50), + company_name VARCHAR(255) NOT NULL, + legal_address TEXT, + inn VARCHAR(20) UNIQUE NOT NULL, + director_name VARCHAR(255), + phone VARCHAR(50), + email VARCHAR(255), + rating INTEGER NOT NULL DEFAULT 0 CHECK (rating >= 0 AND rating <= 100), + sales_locations TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + + # Создание таблицы продаж + sales_table = """ + CREATE TABLE IF NOT EXISTS sales ( + sale_id SERIAL PRIMARY KEY, + partner_id INTEGER NOT NULL REFERENCES partners(partner_id) ON DELETE CASCADE, + product_name VARCHAR(255) NOT NULL, + quantity DECIMAL(15,2) NOT NULL, + sale_date DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + + # Создание таблицы менеджеров + managers_table = """ + CREATE TABLE IF NOT EXISTS managers ( + manager_id SERIAL PRIMARY KEY, + username VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + full_name VARCHAR(255) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + + try: + # Удаляем существующие таблицы + try: + db.execute_query(drop_tables) + print("✅ Существующие таблицы удалены") + except Exception as e: + print(f"ℹ️ Таблицы для удаления не найдены: {e}") + + # Создание таблиц + db.execute_query(partners_table) + db.execute_query(sales_table) + db.execute_query(managers_table) + print("✅ База данных успешно инициализирована") + + # Создание тестового менеджера + password = "pass123" + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + + db.execute_query(""" + INSERT INTO managers (username, password_hash, full_name) + VALUES ('manager', %s, 'Тестовый Менеджер') + ON CONFLICT (username) DO NOTHING + """, (hashed_password,)) + print("✅ Тестовый пользователь создан (manager/pass123)") + + # Добавление тестовых партнеров + test_partners = [ + { + 'partner_type': 'distributor', + 'company_name': 'ООО "Ромашка"', + 'legal_address': 'г. Москва, ул. Ленина, д. 1', + 'inn': '1234567890', + 'director_name': 'Иванов Иван Иванович', + 'phone': '+79991234567', + 'email': 'info@romashka.ru', + 'rating': 85, # INTEGER значение от 0 до 100 + 'sales_locations': 'Москва, Санкт-Петербург' + }, + { + 'partner_type': 'retail', + 'company_name': 'ИП Петров', + 'legal_address': 'г. Санкт-Петербург, Невский пр., д. 100', + 'inn': '0987654321', + 'director_name': 'Петров Петр Петрович', + 'phone': '+79998765432', + 'email': 'petrov@mail.ru', + 'rating': 72, # INTEGER значение от 0 до 100 + 'sales_locations': 'Санкт-Петербург' + } + ] + + for partner in test_partners: + db.execute_query(""" + INSERT INTO partners + (partner_type, company_name, legal_address, inn, director_name, + phone, email, rating, sales_locations) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + partner['partner_type'], partner['company_name'], + partner['legal_address'], partner['inn'], + partner['director_name'], partner['phone'], + partner['email'], partner['rating'], + partner['sales_locations'] + )) + + print("✅ Тестовые партнеры добавлены") + + # Добавление тестовых продаж + test_sales = [ + (1, 'Продукт А', 150.50, '2024-01-15'), + (1, 'Продукт Б', 75.25, '2024-01-16'), + (2, 'Продукт В', 200.00, '2024-01-17'), + (1, 'Продукт А', 100.00, '2024-01-18') + ] + + for sale in test_sales: + db.execute_query(""" + INSERT INTO sales (partner_id, product_name, quantity, sale_date) + VALUES (%s, %s, %s, %s) + """, sale) + + print("✅ Тестовые продажи добавлены") + + # Проверяем, что данные корректно добавлены + partners_count = db.execute_query("SELECT COUNT(*) as count FROM partners")[0]['count'] + sales_count = db.execute_query("SELECT COUNT(*) as count FROM sales")[0]['count'] + managers_count = db.execute_query("SELECT COUNT(*) as count FROM managers")[0]['count'] + + print(f"📊 Статистика базы данных:") + print(f" - Партнеров: {partners_count}") + print(f" - Продаж: {sales_count}") + print(f" - Менеджеров: {managers_count}") + + return True + + except Exception as e: + print(f"❌ Ошибка инициализации базы данных: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """Основная функция""" + args = parse_arguments() + + # Формируем URL подключения + db_url = f"postgresql://{args.username}:{args.password}@{args.host}:{args.port}/{args.database}" + + print(f"🔄 Подключение к базе данных: {args.database} на {args.host}:{args.port}") + + success = initialize_database(db_url) + + if success: + print("🎉 Инициализация базы данных завершена успешно!") + sys.exit(0) + else: + print("💥 Инициализация базы данных завершена с ошибками!") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/ressult/gui/__init__.py b/ressult/gui/__init__.py new file mode 100644 index 0000000..7496f80 --- /dev/null +++ b/ressult/gui/__init__.py @@ -0,0 +1,9 @@ +# gui/__init__.py +""" +Пакет графического интерфейса с авторизацией +""" +from .login_window import LoginWindow +from .main_window import MainWindow +from .partner_form import PartnerForm +from .sales_history import SalesHistoryWindow +from .material_calculator import MaterialCalculatorWindow diff --git a/ressult/gui/__pycache__/__init__.cpython-314.pyc b/ressult/gui/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..844ae5c3cc88738a3c8567bf8dca0c3ab8ab4175 GIT binary patch literal 538 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 literal 0 HcmV?d00001 diff --git a/ressult/gui/__pycache__/main_window.cpython-314.pyc b/ressult/gui/__pycache__/main_window.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd6114ab0deeae3093a239dfd015b272ca2b7623 GIT binary patch literal 29032 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 W&e8AZ{mw|9BmaEW?>YKqRrvp^&L*z_ literal 0 HcmV?d00001 diff --git a/ressult/gui/__pycache__/material_calculator.cpython-314.pyc b/ressult/gui/__pycache__/material_calculator.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5a2627f7fecd2b3d311ad63e1ba6800fd5d2604 GIT binary patch literal 9382 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 literal 0 HcmV?d00001 diff --git a/ressult/gui/__pycache__/partner_form.cpython-314.pyc b/ressult/gui/__pycache__/partner_form.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63a8845dfd4a2a2a9970b9823a9dc3838973d26d GIT binary patch literal 12301 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 a%!IDx-w68y<7vH))?IA-Jt0*jlK%&}`753P literal 0 HcmV?d00001 diff --git a/ressult/gui/__pycache__/sales_history.cpython-314.pyc b/ressult/gui/__pycache__/sales_history.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bb9689f542e6a960eb3c0179e1fc8e864934374 GIT binary patch literal 6145 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 MKi&8h!O~FpU&K}m_W%F@ literal 0 HcmV?d00001 diff --git a/ressult/gui/login_window.py b/ressult/gui/login_window.py new file mode 100644 index 0000000..c61b70b --- /dev/null +++ b/ressult/gui/login_window.py @@ -0,0 +1,253 @@ +# gui/login_window.py +""" +Окно авторизации менеджера +Соответствует требованиям ТЗ по аутентификации +""" +import sys +from PyQt6.QtWidgets import (QApplication, QDialog, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QMessageBox, + QFrame, QCheckBox) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont, QPixmap, QIcon +import requests +from requests.auth import HTTPBasicAuth + +class LoginWindow(QDialog): + """Окно авторизации системы MasterPol""" + + login_success = pyqtSignal(dict) # Сигнал об успешной авторизации + + def __init__(self): + super().__init__() + self.setup_ui() + self.load_settings() + + def setup_ui(self): + """Настройка интерфейса окна авторизации""" + self.setWindowTitle("MasterPol - Авторизация") + self.setFixedSize(400, 500) + self.setModal(True) + + # Установка иконки приложения + try: + self.setWindowIcon(QIcon("resources/icon.png")) + except: + pass + + layout = QVBoxLayout() + layout.setContentsMargins(30, 30, 30, 30) + layout.setSpacing(0) + + # Заголовок + title_label = QLabel("MasterPol") + title_label.setFont(QFont("Arial", 24, QFont.Weight.Bold)) + title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + title_label.setStyleSheet("color: #007acc; margin-bottom: 20px;") + + subtitle_label = QLabel("Система управления партнерами") + subtitle_label.setFont(QFont("Arial", 12)) + subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + subtitle_label.setStyleSheet("color: #666; margin-bottom: 30px;") + + layout.addWidget(title_label) + layout.addWidget(subtitle_label) + + # Форма авторизаци + form_frame = QFrame() + form_frame.setStyleSheet(""" + QFrame { + background-color: white; + border: 0px solid #ddd; + border-radius: 4px; + padding: 20px; + } + """) + + form_layout = QVBoxLayout() + form_layout.setSpacing(15) + + # Поле логина + username_layout = QVBoxLayout() + username_label = QLabel("Имя пользователя:") + username_label.setStyleSheet("font-weight: bold; color: #333;") + + self.username_input = QLineEdit() + self.username_input.setPlaceholderText("Введите имя пользователя") + self.username_input.setStyleSheet(""" + QLineEdit { + padding: 8px 12px; + border: 2px solid #ccc; + border-radius: 6px; + font-size: 14px; + } + QLineEdit:focus { + border-color: #007acc; + } + """) + + username_layout.addWidget(username_label) + username_layout.addWidget(self.username_input) + + # Поле пароля + password_layout = QVBoxLayout() + password_label = QLabel("Пароль:") + password_label.setStyleSheet("font-weight: bold; color: #333;") + + self.password_input = QLineEdit() + self.password_input.setPlaceholderText("Введите пароль") + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) + self.password_input.setStyleSheet(""" + QLineEdit { + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + } + QLineEdit:focus { + border-color: #007acc; + } + """) + + password_layout.addWidget(password_label) + password_layout.addWidget(self.password_input) + + # Запомнить меня + self.remember_checkbox = QCheckBox("Запомнить меня") + self.remember_checkbox.setStyleSheet("color: #333;") + + # Кнопка входа + self.login_button = QPushButton("Войти в систему") + self.login_button.clicked.connect(self.authenticate) + self.login_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 12px; + border-radius: 4px; + font-weight: bold; + font-size: 14px; + } + QPushButton:hover { + background-color: #005a9e; + } + QPushButton:disabled { + background-color: #ccc; + color: #666; + } + """) + + # Подсказка + hint_label = QLabel("Используйте логин: manager, пароль: pass123") + hint_label.setStyleSheet("color: #666; font-size: 12px; margin-top: 10px;") + hint_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + form_layout.addLayout(username_layout) + form_layout.addLayout(password_layout) + form_layout.addWidget(self.remember_checkbox) + form_layout.addWidget(self.login_button) + form_layout.addWidget(hint_label) + + form_frame.setLayout(form_layout) + layout.addWidget(form_frame) + + # Информация о системе + info_label = QLabel("MasterPol v1.0.0\nСистема управления партнерами и продажами") + info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + info_label.setStyleSheet("color: #999; font-size: 11px; margin-top: 20px;") + layout.addWidget(info_label) + + self.setLayout(layout) + + # Подключаем обработчики событий + self.username_input.returnPressed.connect(self.authenticate) + self.password_input.returnPressed.connect(self.authenticate) + + def load_settings(self): + """Загрузка сохраненных настроек авторизации""" + try: + # Здесь можно добавить загрузку из файла настроек + # Пока просто устанавливаем значения по умолчанию + self.username_input.setText("manager") + except: + pass + + def save_settings(self): + """Сохранение настроек авторизации""" + if self.remember_checkbox.isChecked(): + # Здесь можно добавить сохранение в файл настроек + pass + + def authenticate(self): + """Аутентификация пользователя""" + username = self.username_input.text().strip() + password = self.password_input.text().strip() + + if not username or not password: + QMessageBox.warning(self, "Ошибка", "Заполните все поля") + return + + # Блокируем кнопку во время аутентификации + self.login_button.setEnabled(False) + self.login_button.setText("Проверка...") + + try: + # Выполняем аутентификацию через API + response = requests.post( + "http://localhost:8000/api/v1/auth/login", + auth=HTTPBasicAuth(username, password), + timeout=10 + ) + + if response.status_code == 200: + user_data = response.json() + + # Сохраняем настройки + self.save_settings() + + # Сохраняем учетные данные для будущих запросов + user_data['auth'] = HTTPBasicAuth(username, password) + + # Отправляем сигнал об успешной авторизации + self.login_success.emit(user_data) + + else: + QMessageBox.warning( + self, + "Ошибка авторизации", + "Неверное имя пользователя или пароль" + ) + + except requests.exceptions.ConnectionError: + QMessageBox.critical( + self, + "Ошибка подключения", + "Не удалось подключиться к серверу.\n" + "Убедитесь, что сервер запущен на localhost:8000" + ) + except requests.exceptions.Timeout: + QMessageBox.critical( + self, + "Ошибка подключения", + "Превышено время ожидания ответа от сервера" + ) + except Exception as e: + QMessageBox.critical( + self, + "Ошибка", + f"Произошла непредвиденная ошибка:\n{str(e)}" + ) + finally: + # Разблокируем кнопку + self.login_button.setEnabled(True) + self.login_button.setText("Войти в систему") + +def main(): + """Точка входа для тестирования окна авторизации""" + app = QApplication(sys.argv) + window = LoginWindow() + window.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/ressult/gui/main_window b/ressult/gui/main_window new file mode 100644 index 0000000..8af89e8 --- /dev/null +++ b/ressult/gui/main_window @@ -0,0 +1,574 @@ +# gui/main_window.py +""" +Главное окно приложения PyQt6 с поддержкой авторизации +""" +import sys +import requests +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QLabel, QPushButton, QListWidget, + QListWidgetItem, QMessageBox, QFrame, QStackedWidget, + QMenuBar, QMenu, QStatusBar, QToolBar) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction +from .partner_form import PartnerForm +from .sales_history import SalesHistoryWindow +from .material_calculator import MaterialCalculatorWindow + +class PartnerCard(QFrame): + """Карточка партнера для отображения в списке""" + partner_clicked = pyqtSignal(dict) + + def __init__(self, partner_data): + super().__init__() + self.partner_data = partner_data + self.setup_ui() + + def setup_ui(self): + self.setFrameStyle(QFrame.Shape.StyledPanel) + self.setStyleSheet(""" + PartnerCard { + background-color: white; + border: 1px solid #ddd; + border-radius: 8px; + padding: 12px; + margin: 4px; + } + PartnerCard:hover { + background-color: #f5f5f5; + border-color: #007acc; + } + """) + + layout = QVBoxLayout() + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(4) + + # Заголовок с типом и названием + header_layout = QHBoxLayout() + header_layout.setSpacing(4) + + type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |") + type_label.setStyleSheet("color: #666; font-weight: bold;") + + name_label = QLabel(self.partner_data['company_name']) + name_label.setStyleSheet("font-weight: bold; font-size: 14px;") + name_label.setWordWrap(True) + + # Безопасное преобразование рейтинга + rating_value = self.partner_data.get('rating', 0) + if isinstance(rating_value, float): + rating_value = int(rating_value) + + rating_label = QLabel(f"{rating_value}%") + rating_label.setStyleSheet("color: #007acc; font-weight: bold;") + + header_layout.addWidget(type_label) + header_layout.addWidget(name_label) + header_layout.addStretch() + header_layout.addWidget(rating_label) + + # Информация о директоре + director_label = QLabel(self.partner_data.get('director_name', 'Директор не указан')) + director_label.setStyleSheet("color: #444;") + + # Контактная информация + phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан')) + phone_label.setStyleSheet("color: #666;") + + layout.addLayout(header_layout) + layout.addWidget(director_label) + layout.addWidget(phone_label) + + self.setLayout(layout) + + def mousePressEvent(self, event): + """Обработка клика на карточке""" + if event.button() == Qt.MouseButton.LeftButton: + self.partner_clicked.emit(self.partner_data) + +class MainWindow(QMainWindow): + """Главное окно приложения с поддержкой авторизации""" + + def __init__(self, user_data): + super().__init__() + self.user_data = user_data + self.current_partner = None + self.auth = user_data.get('auth') + self.setup_ui() + self.load_partners() + + def setup_ui(self): + """Настройка интерфейса главного окна""" + self.setWindowTitle(f"MasterPol - Система управления партнерами") + self.setGeometry(100, 100, 1200, 700) + + # Установка иконки приложения + try: + self.setWindowIcon(QIcon("resources/icon.png")) + except: + pass + + # Создание меню + self.create_menu() + + # Создание тулбара + self.create_toolbar() + + # Создание статусной строки + self.create_statusbar() + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + main_layout = QHBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + + # Левая панель - список партнеров + left_panel = self.create_partners_panel() + main_layout.addWidget(left_panel, 1) + + # Правая панель - детальная информация + self.right_panel = self.create_details_panel() + main_layout.addWidget(self.right_panel, 2) + + central_widget.setLayout(main_layout) + + def create_menu(self): + """Создание меню приложения""" + menubar = self.menuBar() + + # Меню Файл + file_menu = menubar.addMenu('Файл') + + refresh_action = QAction('Обновить', self) + refresh_action.setShortcut('F5') + refresh_action.triggered.connect(self.load_partners) + file_menu.addAction(refresh_action) + + file_menu.addSeparator() + + logout_action = QAction('Выход', self) + logout_action.setShortcut('Ctrl+Q') + logout_action.triggered.connect(self.logout) + file_menu.addAction(logout_action) + + # Меню Сервис + service_menu = menubar.addMenu('Сервис') + + calc_action = QAction('Калькулятор материалов', self) + calc_action.triggered.connect(self.show_material_calculator) + service_menu.addAction(calc_action) + + # Меню Справка + help_menu = menubar.addMenu('Справка') + + about_action = QAction('О программе', self) + about_action.triggered.connect(self.show_about) + help_menu.addAction(about_action) + + def create_toolbar(self): + """Создание панели инструментов""" + toolbar = QToolBar("Основные инструменты") + self.addToolBar(toolbar) + + refresh_action = QAction('Обновить', self) + refresh_action.triggered.connect(self.load_partners) + toolbar.addAction(refresh_action) + + toolbar.addSeparator() + + add_partner_action = QAction('Добавить партнера', self) + add_partner_action.triggered.connect(self.show_add_partner_form) + toolbar.addAction(add_partner_action) + + def create_statusbar(self): + """Создание статусной строки""" + statusbar = self.statusBar() + user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}" + statusbar.showMessage(user_info) + + def create_partners_panel(self): + """Создание панели списка партнеров""" + panel = QWidget() + panel.setMaximumWidth(400) + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок + title = QLabel("Партнеры") + title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("padding: 10px;") + layout.addWidget(title) + + # Панель управления + control_layout = QHBoxLayout() + control_layout.setSpacing(10) + + self.add_button = QPushButton("Добавить партнера") + self.add_button.clicked.connect(self.show_add_partner_form) + self.add_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.refresh_button = QPushButton("Обновить") + self.refresh_button.clicked.connect(self.load_partners) + self.refresh_button.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #545b62; + } + """) + + control_layout.addWidget(self.add_button) + control_layout.addWidget(self.refresh_button) + control_layout.addStretch() + + layout.addLayout(control_layout) + + # Список партнеров + self.partners_list = QListWidget() + self.partners_list.setStyleSheet(""" + QListWidget { + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + outline: none; + } + QListWidget::item { + border: none; + padding: 0px; + } + QListWidget::item:selected { + background-color: transparent; + } + """) + layout.addWidget(self.partners_list) + + # Кнопка расчета материалов + self.calc_button = QPushButton("Калькулятор материалов") + self.calc_button.clicked.connect(self.show_material_calculator) + self.calc_button.setStyleSheet(""" + QPushButton { + background-color: #17a2b8; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #138496; + } + """) + layout.addWidget(self.calc_button) + + panel.setLayout(layout) + return panel + + def create_details_panel(self): + """Создание панели детальной информации""" + panel = QWidget() + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок детальной информации + self.details_title = QLabel("Выберите партнера") + self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold)) + self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.details_title.setStyleSheet("padding: 10px;") + layout.addWidget(self.details_title) + + # Детальная информация о партнере - создаем пустой frame + self.details_frame = QFrame() + self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + self.details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + self.details_layout = QVBoxLayout() + self.details_layout.setSpacing(8) + self.details_frame.setLayout(self.details_layout) + self.details_frame.hide() + + layout.addWidget(self.details_frame) + + # Кнопки управления выбранным партнером + self.control_buttons = QWidget() + buttons_layout = QHBoxLayout() + buttons_layout.setSpacing(10) + + self.edit_button = QPushButton("Редактировать") + self.edit_button.clicked.connect(self.edit_partner) + self.edit_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + self.edit_button.hide() + + self.sales_button = QPushButton("История продаж") + self.sales_button.clicked.connect(self.show_sales_history) + self.sales_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + self.sales_button.hide() + + self.discount_button = QPushButton("Расчет скидки") + self.discount_button.clicked.connect(self.calculate_discount) + self.discount_button.setStyleSheet(""" + QPushButton { + background-color: #ffc107; + color: black; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #e0a800; + } + """) + self.discount_button.hide() + + buttons_layout.addWidget(self.edit_button) + buttons_layout.addWidget(self.sales_button) + buttons_layout.addWidget(self.discount_button) + buttons_layout.addStretch() + + self.control_buttons.setLayout(buttons_layout) + layout.addWidget(self.control_buttons) + + # Добавляем растягивающийся элемент в конец + layout.addStretch() + + panel.setLayout(layout) + return panel + + def load_partners(self): + """Загрузка списка партнеров из API с авторизацией""" + try: + response = requests.get( + "http://localhost:8000/api/v1/partners", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.partners_list.clear() + partners = response.json() + + for partner in partners: + item = QListWidgetItem() + card = PartnerCard(partner) + card.partner_clicked.connect(self.show_partner_details) + + # Устанавливаем фиксированный размер для элемента + item.setSizeHint(card.sizeHint()) + self.partners_list.addItem(item) + self.partners_list.setItemWidget(item, card) + + # Сбрасываем выделение + self.partners_list.clearSelection() + self.current_partner = None + self.details_title.setText("Выберите партнера") + self.details_frame.hide() + self.edit_button.hide() + self.sales_button.hide() + self.discount_button.hide() + + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + else: + QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}") + + def show_partner_details(self, partner_data): + """Отображение детальной информации о партнере""" + self.current_partner = partner_data + self.details_title.setText(partner_data['company_name']) + + # Создаем новый виджет для деталей вместо очистки layout + new_details_frame = QFrame() + new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + new_details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + new_details_layout = QVBoxLayout() + new_details_layout.setSpacing(8) + + # Добавляем новую информацию + details = [ + ("Тип:", partner_data.get('partner_type', 'Не указан')), + ("ИНН:", partner_data.get('inn', 'Не указан')), + ("Директор:", partner_data.get('director_name', 'Не указан')), + ("Телефон:", partner_data.get('phone', 'Не указан')), + ("Email:", partner_data.get('email', 'Не указан')), + ("Рейтинг:", str(partner_data.get('rating', 0))), + ("Адрес:", partner_data.get('legal_address', 'Не указан')), + ("Регионы:", partner_data.get('sales_locations', 'Не указан')) + ] + + for label, value in details: + row_widget = QWidget() + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(0, 2, 0, 2) + + label_widget = QLabel(label) + label_widget.setStyleSheet("font-weight: bold; min-width: 100px;") + label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + value_widget = QLabel(str(value)) + value_widget.setWordWrap(True) + value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + row_layout.addWidget(label_widget) + row_layout.addWidget(value_widget) + row_layout.addStretch() + + new_details_layout.addWidget(row_widget) + + new_details_frame.setLayout(new_details_layout) + + # Заменяем старый details_frame на новый + old_frame = self.details_frame + layout = self.right_panel.layout() + layout.replaceWidget(old_frame, new_details_frame) + old_frame.deleteLater() + + self.details_frame = new_details_frame + self.details_layout = new_details_layout + + self.details_frame.show() + self.edit_button.show() + self.sales_button.show() + self.discount_button.show() + + def show_add_partner_form(self): + """Открытие формы добавления партнера""" + form = PartnerForm(self, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def edit_partner(self): + """Редактирование выбранного партнера""" + if self.current_partner: + form = PartnerForm(self, self.current_partner, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def show_sales_history(self): + """Открытие истории продаж партнера""" + if self.current_partner: + sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth) + sales_window.exec() + + def calculate_discount(self): + """Расчет скидки для партнера с авторизацией""" + if self.current_partner: + try: + response = requests.get( + f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + discount_data = response.json() + QMessageBox.information( + self, + "Расчет скидки", + f"Партнер: {self.current_partner['company_name']}\n" + f"Общие продажи: {discount_data['total_sales']}\n" + f"Скидка: {discount_data['discount_percent']}%" + ) + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}") + + def show_material_calculator(self): + """Открытие калькулятора материалов""" + calculator = MaterialCalculatorWindow(self, auth=self.auth) + calculator.exec() + + def logout(self): + """Выход из системы""" + reply = QMessageBox.question( + self, + "Подтверждение выхода", + "Вы уверены, что хотите выйти из системы?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + self.close() + # Здесь можно добавить вызов окна авторизации + # или перезапуск приложения + + def show_about(self): + """Показать информацию о программе""" + QMessageBox.about( + self, + "О программе MasterPol", + "MasterPol - Система управления партнерами\n\n" + "Версия: 1.0.0\n" + "Разработчик: Команда MasterPol\n\n" + "Система предназначена для управления партнерами,\n" + "учета продаж и расчета бизнес-показателей." + ) diff --git a/ressult/gui/main_window.py b/ressult/gui/main_window.py new file mode 100644 index 0000000..8af89e8 --- /dev/null +++ b/ressult/gui/main_window.py @@ -0,0 +1,574 @@ +# gui/main_window.py +""" +Главное окно приложения PyQt6 с поддержкой авторизации +""" +import sys +import requests +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QLabel, QPushButton, QListWidget, + QListWidgetItem, QMessageBox, QFrame, QStackedWidget, + QMenuBar, QMenu, QStatusBar, QToolBar) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction +from .partner_form import PartnerForm +from .sales_history import SalesHistoryWindow +from .material_calculator import MaterialCalculatorWindow + +class PartnerCard(QFrame): + """Карточка партнера для отображения в списке""" + partner_clicked = pyqtSignal(dict) + + def __init__(self, partner_data): + super().__init__() + self.partner_data = partner_data + self.setup_ui() + + def setup_ui(self): + self.setFrameStyle(QFrame.Shape.StyledPanel) + self.setStyleSheet(""" + PartnerCard { + background-color: white; + border: 1px solid #ddd; + border-radius: 8px; + padding: 12px; + margin: 4px; + } + PartnerCard:hover { + background-color: #f5f5f5; + border-color: #007acc; + } + """) + + layout = QVBoxLayout() + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(4) + + # Заголовок с типом и названием + header_layout = QHBoxLayout() + header_layout.setSpacing(4) + + type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |") + type_label.setStyleSheet("color: #666; font-weight: bold;") + + name_label = QLabel(self.partner_data['company_name']) + name_label.setStyleSheet("font-weight: bold; font-size: 14px;") + name_label.setWordWrap(True) + + # Безопасное преобразование рейтинга + rating_value = self.partner_data.get('rating', 0) + if isinstance(rating_value, float): + rating_value = int(rating_value) + + rating_label = QLabel(f"{rating_value}%") + rating_label.setStyleSheet("color: #007acc; font-weight: bold;") + + header_layout.addWidget(type_label) + header_layout.addWidget(name_label) + header_layout.addStretch() + header_layout.addWidget(rating_label) + + # Информация о директоре + director_label = QLabel(self.partner_data.get('director_name', 'Директор не указан')) + director_label.setStyleSheet("color: #444;") + + # Контактная информация + phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан')) + phone_label.setStyleSheet("color: #666;") + + layout.addLayout(header_layout) + layout.addWidget(director_label) + layout.addWidget(phone_label) + + self.setLayout(layout) + + def mousePressEvent(self, event): + """Обработка клика на карточке""" + if event.button() == Qt.MouseButton.LeftButton: + self.partner_clicked.emit(self.partner_data) + +class MainWindow(QMainWindow): + """Главное окно приложения с поддержкой авторизации""" + + def __init__(self, user_data): + super().__init__() + self.user_data = user_data + self.current_partner = None + self.auth = user_data.get('auth') + self.setup_ui() + self.load_partners() + + def setup_ui(self): + """Настройка интерфейса главного окна""" + self.setWindowTitle(f"MasterPol - Система управления партнерами") + self.setGeometry(100, 100, 1200, 700) + + # Установка иконки приложения + try: + self.setWindowIcon(QIcon("resources/icon.png")) + except: + pass + + # Создание меню + self.create_menu() + + # Создание тулбара + self.create_toolbar() + + # Создание статусной строки + self.create_statusbar() + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + main_layout = QHBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + + # Левая панель - список партнеров + left_panel = self.create_partners_panel() + main_layout.addWidget(left_panel, 1) + + # Правая панель - детальная информация + self.right_panel = self.create_details_panel() + main_layout.addWidget(self.right_panel, 2) + + central_widget.setLayout(main_layout) + + def create_menu(self): + """Создание меню приложения""" + menubar = self.menuBar() + + # Меню Файл + file_menu = menubar.addMenu('Файл') + + refresh_action = QAction('Обновить', self) + refresh_action.setShortcut('F5') + refresh_action.triggered.connect(self.load_partners) + file_menu.addAction(refresh_action) + + file_menu.addSeparator() + + logout_action = QAction('Выход', self) + logout_action.setShortcut('Ctrl+Q') + logout_action.triggered.connect(self.logout) + file_menu.addAction(logout_action) + + # Меню Сервис + service_menu = menubar.addMenu('Сервис') + + calc_action = QAction('Калькулятор материалов', self) + calc_action.triggered.connect(self.show_material_calculator) + service_menu.addAction(calc_action) + + # Меню Справка + help_menu = menubar.addMenu('Справка') + + about_action = QAction('О программе', self) + about_action.triggered.connect(self.show_about) + help_menu.addAction(about_action) + + def create_toolbar(self): + """Создание панели инструментов""" + toolbar = QToolBar("Основные инструменты") + self.addToolBar(toolbar) + + refresh_action = QAction('Обновить', self) + refresh_action.triggered.connect(self.load_partners) + toolbar.addAction(refresh_action) + + toolbar.addSeparator() + + add_partner_action = QAction('Добавить партнера', self) + add_partner_action.triggered.connect(self.show_add_partner_form) + toolbar.addAction(add_partner_action) + + def create_statusbar(self): + """Создание статусной строки""" + statusbar = self.statusBar() + user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}" + statusbar.showMessage(user_info) + + def create_partners_panel(self): + """Создание панели списка партнеров""" + panel = QWidget() + panel.setMaximumWidth(400) + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок + title = QLabel("Партнеры") + title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("padding: 10px;") + layout.addWidget(title) + + # Панель управления + control_layout = QHBoxLayout() + control_layout.setSpacing(10) + + self.add_button = QPushButton("Добавить партнера") + self.add_button.clicked.connect(self.show_add_partner_form) + self.add_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.refresh_button = QPushButton("Обновить") + self.refresh_button.clicked.connect(self.load_partners) + self.refresh_button.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #545b62; + } + """) + + control_layout.addWidget(self.add_button) + control_layout.addWidget(self.refresh_button) + control_layout.addStretch() + + layout.addLayout(control_layout) + + # Список партнеров + self.partners_list = QListWidget() + self.partners_list.setStyleSheet(""" + QListWidget { + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + outline: none; + } + QListWidget::item { + border: none; + padding: 0px; + } + QListWidget::item:selected { + background-color: transparent; + } + """) + layout.addWidget(self.partners_list) + + # Кнопка расчета материалов + self.calc_button = QPushButton("Калькулятор материалов") + self.calc_button.clicked.connect(self.show_material_calculator) + self.calc_button.setStyleSheet(""" + QPushButton { + background-color: #17a2b8; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #138496; + } + """) + layout.addWidget(self.calc_button) + + panel.setLayout(layout) + return panel + + def create_details_panel(self): + """Создание панели детальной информации""" + panel = QWidget() + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок детальной информации + self.details_title = QLabel("Выберите партнера") + self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold)) + self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.details_title.setStyleSheet("padding: 10px;") + layout.addWidget(self.details_title) + + # Детальная информация о партнере - создаем пустой frame + self.details_frame = QFrame() + self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + self.details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + self.details_layout = QVBoxLayout() + self.details_layout.setSpacing(8) + self.details_frame.setLayout(self.details_layout) + self.details_frame.hide() + + layout.addWidget(self.details_frame) + + # Кнопки управления выбранным партнером + self.control_buttons = QWidget() + buttons_layout = QHBoxLayout() + buttons_layout.setSpacing(10) + + self.edit_button = QPushButton("Редактировать") + self.edit_button.clicked.connect(self.edit_partner) + self.edit_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + self.edit_button.hide() + + self.sales_button = QPushButton("История продаж") + self.sales_button.clicked.connect(self.show_sales_history) + self.sales_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + self.sales_button.hide() + + self.discount_button = QPushButton("Расчет скидки") + self.discount_button.clicked.connect(self.calculate_discount) + self.discount_button.setStyleSheet(""" + QPushButton { + background-color: #ffc107; + color: black; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #e0a800; + } + """) + self.discount_button.hide() + + buttons_layout.addWidget(self.edit_button) + buttons_layout.addWidget(self.sales_button) + buttons_layout.addWidget(self.discount_button) + buttons_layout.addStretch() + + self.control_buttons.setLayout(buttons_layout) + layout.addWidget(self.control_buttons) + + # Добавляем растягивающийся элемент в конец + layout.addStretch() + + panel.setLayout(layout) + return panel + + def load_partners(self): + """Загрузка списка партнеров из API с авторизацией""" + try: + response = requests.get( + "http://localhost:8000/api/v1/partners", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.partners_list.clear() + partners = response.json() + + for partner in partners: + item = QListWidgetItem() + card = PartnerCard(partner) + card.partner_clicked.connect(self.show_partner_details) + + # Устанавливаем фиксированный размер для элемента + item.setSizeHint(card.sizeHint()) + self.partners_list.addItem(item) + self.partners_list.setItemWidget(item, card) + + # Сбрасываем выделение + self.partners_list.clearSelection() + self.current_partner = None + self.details_title.setText("Выберите партнера") + self.details_frame.hide() + self.edit_button.hide() + self.sales_button.hide() + self.discount_button.hide() + + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + else: + QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}") + + def show_partner_details(self, partner_data): + """Отображение детальной информации о партнере""" + self.current_partner = partner_data + self.details_title.setText(partner_data['company_name']) + + # Создаем новый виджет для деталей вместо очистки layout + new_details_frame = QFrame() + new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + new_details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + new_details_layout = QVBoxLayout() + new_details_layout.setSpacing(8) + + # Добавляем новую информацию + details = [ + ("Тип:", partner_data.get('partner_type', 'Не указан')), + ("ИНН:", partner_data.get('inn', 'Не указан')), + ("Директор:", partner_data.get('director_name', 'Не указан')), + ("Телефон:", partner_data.get('phone', 'Не указан')), + ("Email:", partner_data.get('email', 'Не указан')), + ("Рейтинг:", str(partner_data.get('rating', 0))), + ("Адрес:", partner_data.get('legal_address', 'Не указан')), + ("Регионы:", partner_data.get('sales_locations', 'Не указан')) + ] + + for label, value in details: + row_widget = QWidget() + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(0, 2, 0, 2) + + label_widget = QLabel(label) + label_widget.setStyleSheet("font-weight: bold; min-width: 100px;") + label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + value_widget = QLabel(str(value)) + value_widget.setWordWrap(True) + value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + row_layout.addWidget(label_widget) + row_layout.addWidget(value_widget) + row_layout.addStretch() + + new_details_layout.addWidget(row_widget) + + new_details_frame.setLayout(new_details_layout) + + # Заменяем старый details_frame на новый + old_frame = self.details_frame + layout = self.right_panel.layout() + layout.replaceWidget(old_frame, new_details_frame) + old_frame.deleteLater() + + self.details_frame = new_details_frame + self.details_layout = new_details_layout + + self.details_frame.show() + self.edit_button.show() + self.sales_button.show() + self.discount_button.show() + + def show_add_partner_form(self): + """Открытие формы добавления партнера""" + form = PartnerForm(self, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def edit_partner(self): + """Редактирование выбранного партнера""" + if self.current_partner: + form = PartnerForm(self, self.current_partner, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def show_sales_history(self): + """Открытие истории продаж партнера""" + if self.current_partner: + sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth) + sales_window.exec() + + def calculate_discount(self): + """Расчет скидки для партнера с авторизацией""" + if self.current_partner: + try: + response = requests.get( + f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + discount_data = response.json() + QMessageBox.information( + self, + "Расчет скидки", + f"Партнер: {self.current_partner['company_name']}\n" + f"Общие продажи: {discount_data['total_sales']}\n" + f"Скидка: {discount_data['discount_percent']}%" + ) + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}") + + def show_material_calculator(self): + """Открытие калькулятора материалов""" + calculator = MaterialCalculatorWindow(self, auth=self.auth) + calculator.exec() + + def logout(self): + """Выход из системы""" + reply = QMessageBox.question( + self, + "Подтверждение выхода", + "Вы уверены, что хотите выйти из системы?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + self.close() + # Здесь можно добавить вызов окна авторизации + # или перезапуск приложения + + def show_about(self): + """Показать информацию о программе""" + QMessageBox.about( + self, + "О программе MasterPol", + "MasterPol - Система управления партнерами\n\n" + "Версия: 1.0.0\n" + "Разработчик: Команда MasterPol\n\n" + "Система предназначена для управления партнерами,\n" + "учета продаж и расчета бизнес-показателей." + ) diff --git a/ressult/gui/main_window.py.bak b/ressult/gui/main_window.py.bak new file mode 100644 index 0000000..2051605 --- /dev/null +++ b/ressult/gui/main_window.py.bak @@ -0,0 +1,616 @@ +# gui/main_wind/w.py +""" +Главное окно приложения PyQt6 с поддержкой авторизации +""" +import sys +import requests +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QLabel, QPushButton, QListWidget, + QListWidgetItem, QMessageBox, QFrame, QStackedWidget, + QMenuBar, QMenu, QStatusBar, QToolBar) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction +from .partner_form import PartnerForm +from .sales_history import SalesHistoryWindow +from .material_calculator import MaterialCalculatorWindow + +class PartnerCard(QFrame): + """Карточка партнера для отображения в списке""" + partner_clicked = pyqtSignal(dict) + + def __init__(self, partner_data): + super().__init__() + self.partner_data = partner_data + self.setup_ui() + + def setup_ui(self): + self.setFrameStyle(QFrame.Shape.StyledPanel) + self.setStyleSheet(""" + PartnerCard { + background-color: white; + border: 1px solid #ddd; + border-radius: 8px; + padding: 12px; + margin: 4px; + } + PartnerCard:hover { + background-color: #f5f5f5; + border-color: #007acc; + } + """) + + layout = QVBoxLayout() + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(4) + + # Заголовок с типом и названием + header_layout = QHBoxLayout() + header_layout.setSpacing(4) + + type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |") + type_label.setStyleSheet("color: #666; font-weight: bold;") + + name_label = QLabel(self.partner_data['company_name']) + name_label.setStyleSheet("font-weight: bold; font-size: 14px;") + name_label.setWordWrap(True) + + # Безопасное преобразование рейтинга + rating_value = self.partner_data.get('rating', 0) + if isinstance(rating_value, float): + rating_value = int(rating_value) + + rating_label = QLabel(f"{rating_value}%") + rating_label.setStyleSheet("color: #007acc; font-weight: bold;") + + header_layout.addWidget(type_label) + header_layout.addWidget(name_label) + header_layout.addStretch() + header_layout.addWidget(rating_label) + + # Информация о директоре + QLabel(self.partner_data.get('director_name', 'Директор не указан')) + director_label.setStyleSheet("color: #444;") + + # Контактная информация + phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан')) + phone_label.setStyleSheet("color: #666;") + + layout.addLayout(header_layout) + layout.addWidget(director_label) + layout.addWidget(phone_label) + + self.setLayout(layout) + + def mousePressEvent(self, event): + """Обработка клика на карточке""" + if event.button() == Qt.MouseButton.LeftButton: + self.partner_clicked.emit(self.partner_data) + +class MainWindow(QMainWindow): + """Главное окно приложения с поддержкой авторизации""" + + def __init__(self, user_data): + super().__init__() + self.user_data = user_data + self.current_partner = None + self.orders_panel = None + self.auth = user_data.get('auth') + self.setup_ui() + self.load_partners() + + def setup_ui(self): + """Настройка интерфейса главного окна""" + self.setWindowTitle(f"MasterPol - Система управления партнерами") + self.setGeometry(100, 100, 1200, 700) + + # Установка иконки приложения + try: + self.setWindowIcon(QIcon("resources/icon.png")) + except: + pass + + # Создание меню + self.create_menu() + + # Создание тулбара + self.create_toolbar() + + # Создание статусной строки + self.create_statusbar() + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + main_layout = QHBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + + # Левая панель - список партнеров + left_panel = self.create_partners_panel() + main_layout.addWidget(left_panel, 1) + + # Правая панель - детальная информация + self.right_panel = self.create_details_panel() + main_layout.addWidget(self.right_panel, 2) + + central_widget.setLayout(main_layout) + + def create_menu(self): + """Создание меню приложения""" + menubar = self.menuBar() + + # Меню Файл + file_menu = menubar.addMenu('Файл') + + refresh_action = QAction('Обновить', self) + refresh_action.setShortcut('F5') + refresh_action.triggered.connect(self.load_partners) + file_menu.addAction(refresh_action) + + file_menu.addSeparator() + + logout_action = QAction('Выход', self) + logout_action.setShortcut('Ctrl+Q') + logout_action.triggered.connect(self.logout) + file_menu.addAction(logout_action) + + # Меню Сервис + service_menu = menubar.addMenu('Сервис') + + calc_action = QAction('Калькулятор материалов', self) + calc_action.triggered.connect(self.show_material_calculator) + service_menu.addAction(calc_action) + + # Меню Справка + help_menu = menubar.addMenu('Справка') + + about_action = QAction('О программе', self) + about_action.triggered.connect(self.show_about) + help_menu.addAction(about_action) + + def create_toolbar(self): + """Создание панели инструментов""" + toolbar = QToolBar("Основные инструменты") + self.addToolBar(toolbar) + + refresh_action = QAction('Обновить', self) + refresh_action.triggered.connect(self.load_partners) + toolbar.addAction(refresh_action) + + toolbar.addSeparator() + + add_partner_action = QAction('Добавить партнера', self) + add_partner_action.triggered.connect(self.show_add_partner_form) + toolbar.addAction(add_partner_action) + + def create_statusbar(self): + """Создание статусной строки""" + statusbar = self.statusBar() + user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}" + statusbar.showMessage(user_info) + + def create_partners_panel(self): + """Создание панели списка партнеров""" + panel = QWidget() + panel.setMaximumWidth(400) + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок + title = QLabel("Партнеры") + title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("padding: 10px;") + layout.addWidget(title) + + # Панель управления + control_layout = QHBoxLayout() + control_layout.setSpacing(10) + + self.add_button = QPushButton("Добавить партнера") + self.add_button.clicked.connect(self.show_add_partner_form) + self.add_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.refresh_button = QPushButton("Обновить") + self.refresh_button.clicked.connect(self.load_partners) + self.refresh_button.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #545b62; + } + """) + + control_layout.addWidget(self.add_button) + control_layout.addWidget(self.refresh_button) + control_layout.addStretch() + + layout.addLayout(control_layout) + + # Список партнеров + self.partners_list = QListWidget() + self.partners_list.setStyleSheet(""" + QListWidget { + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + outline: none; + } + QListWidget::item { + border: none; + padding: 0px; + } + QListWidget::item:selected { + background-color: transparent; + } + """) + layout.addWidget(self.partners_list) + + # Кнопка расчета материалов + self.calc_button = QPushButton("Калькулятор материалов") + self.calc_button.clicked.connect(self.show_material_calculator) + self.calc_button.setStyleSheet(""" + QPushButton { + background-color: #17a2b8; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #138496; + } + """) + layout.addWidget(self.calc_button) + + panel.setLayout(layout) + return panel + + def create_details_panel(self): + """Создание панели детальной информации""" + panel = QWidget() + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок детальной информации + self.details_title = QLabel("Выберите партнера") + self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold)) + self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.details_title.setStyleSheet("padding: 10px;") + layout.addWidget(self.details_title) + + # Детальная информация о партнере - создаем пустой frame + self.details_frame = QFrame() + self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + self.details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + self.details_layout = QVBoxLayout() + self.details_layout.setSpacing(8) + self.details_frame.setLayout(self.details_layout) + self.details_frame.hide() + + layout.addWidget(self.details_frame) + + # Кнопки управления выбранным партнером + self.control_buttons = QWidget() + buttons_layout = QHBoxLayout() + buttons_layout.setSpacing(10) + + self.edit_button = QPushButton("Редактировать") + self.edit_button.clicked.connect(self.edit_partner) + self.edit_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + self.edit_button.hide() + + self.sales_button = QPushButton("История продаж") + self.sales_button.clicked.connect(self.show_sales_history) + self.sales_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + self.sales_button.hide() + + self.discount_button = QPushButton("Расчет скидки") + self.discount_button.clicked.connect(self.calculate_discount) + self.discount_button.setStyleSheet(""" + QPushButton { + background-color: #ffc107; + color: black; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #e0a800; + } + """) + self.discount_button.hide() + + buttons_layout.addWidget(self.edit_button) + buttons_layout.addWidget(self.sales_button) + buttons_layout.addWidget(self.discount_button) + buttons_layout.addStretch() + + self.control_buttons.setLayout(buttons_layout) + layout.addWidget(self.control_buttons) + + # Добавляем растягивающийся элемент в конец + layout.addStretch() + + panel.setLayout(layout) + return panel + + def load_partners(self): + """Загрузка списка партнеров из API с авторизацией""" + try: + response = requests.get( + "http://localhost:8000/api/v1/partners", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.partners_list.clear() + partners = response.json() + + for partner in partners: + item = QListWidgetItem() + card = PartnerCard(partner) + card.partner_clicked.connect(self.show_partner_details) + + # Устанавливаем фиксированный размер для элемента + item.setSizeHint(card.sizeHint()) + self.partners_list.addItem(item) + self.partners_list.setItemWidget(item, card) + + # Сбрасываем выделение + self.partners_list.clearSelection() + self.current_partner = None + self.details_title.setText("Выберите партнера") + self.details_frame.hide() + self.edit_button.hide() + self.sales_button.hide() + self.discount_button.hide() + + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + else: + QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}") + + def show_partner_details(self, partner_data): + """Отображение детальной информации о партнере""" + self.current_partner = partner_data + self.details_title.setText(partner_data['company_name']) + + # Создаем новый виджет для деталей вместо очистки layout + new_details_frame = QFrame() + new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + new_details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + new_details_layout = QVBoxLayout() + new_details_layout.setSpacing(8) + + # Добавляем новую информацию + details = [ + ("Тип:", partner_data.get('partner_type', 'Не указан')), + ("ИНН:", partner_data.get('inn', 'Не указан')), + ("Директор:", partner_data.get('director_name', 'Не указан')), + ("Телефон:", partner_data.get('phone', 'Не указан')), + ("Email:", partner_data.get('email', 'Не указан')), + ("Рейтинг:", str(partner_data.get('rating', 0))), + ("Адрес:", partner_data.get('legal_address', 'Не указан')), + ("Регионы:", partner_data.get('sales_locations', 'Не указан')) + ] + + # ЗАМЕНИТЕ этот блок кода в методе show_partner_details: +for label, value in details: + row_widget = QWidget() + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(0, 2, 0, 2) + + label_widget = QLabel(label) + label_widget.setStyleSheet("font-weight: bold; min-width: 100px;") + label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + value_widget = QLabel(str(value)) + value_widget.setWordWrap(True) + value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + row_layout.addWidget(label_widget) + row_layout.addWidget(value_widget) + row_layout.addStretch() + + new_details_layout.addWidget(row_widget) + + # НА этот исправленный вариант: + for label, value in details: + # Создаем контейнер для строки + row_container = QWidget() + row_container.setFixedHeight(30) # Фиксированная высота для каждой строки + row_layout = QHBoxLayout(row_container) + row_layout.setContentsMargins(5, 0, 5, 0) + row_layout.setSpacing(10) + + # Лейбл (название поля) + label_widget = QLabel(label) + label_widget.setStyleSheet(""" + QLabel { + font-weight: bold; + color: #333; + min-width: 120px; + max-width: 120px; + background-color: transparent; + } + """) + label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + + # Значение + value_widget = QLabel(str(value)) + value_widget.setStyleSheet(""" + QLabel { + color: #555; + background-color: transparent; + border: none; + } + """) + value_widget.setWordWrap(True) + value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + + row_layout.addWidget(label_widget) + row_layout.addWidget(value_widget) + row_layout.addStretch() + + new_details_layout.addWidget(row_container) + + new_details_frame.setLayout(new_details_layout) + + # Заменяем старый details_frame на новый + old_frame = self.details_frame + layout = self.right_panel.layout() + layout.replaceWidget(old_frame, new_details_frame) + old_frame.deleteLater() + + self.details_frame = new_details_frame + self.details_layout = new_details_layout + + self.details_frame.show() + self.edit_button.show() + self.sales_button.show() + self.discount_button.show() + + def show_add_partner_form(self): + """Открытие формы добавления партнера""" + form = PartnerForm(self, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def edit_partner(self): + """Редактирование выбранного партнера""" + if self.current_partner: + form = PartnerForm(self, self.current_partner, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def show_sales_history(self): + """Открытие истории продаж партнера""" + if self.current_partner: + sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth) + sales_window.exec() + + def calculate_discount(self): + """Расчет скидки для партнера с авторизацией""" + if self.current_partner: + try: + response = requests.get( + f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + discount_data = response.json() + QMessageBox.information( + self, + "Расчет скидки", + f"Партнер: {self.current_partner['company_name']}\n" + f"Общие продажи: {discount_data['total_sales']}\n" + f"Скидка: {discount_data['discount_percent']}%" + ) + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}") + + def show_material_calculator(self): + """Открытие калькулятора материалов""" + calculator = MaterialCalculatorWindow(self, auth=self.auth) + calculator.exec() + + def logout(self): + """Выход из системы""" + reply = QMessageBox.question( + self, + "Подтверждение выхода", + "Вы уверены, что хотите выйти из системы?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + self.close() + # Здесь можно добавить вызов окна авторизации + # или перезапуск приложения + + def show_about(self): + """Показать информацию о программе""" + QMessageBox.about( + self, + "О программе MasterPol", + "MasterPol - Система управления партнерами\n\n" + "Версия: 1.0.0\n" + "Разработчик: Команда MasterPol\n\n" + "Система предназначена для управления партнерами,\n" + "учета продаж и расчета бизнес-показателей." + ) diff --git a/ressult/gui/material_calculator.py b/ressult/gui/material_calculator.py new file mode 100644 index 0000000..6903972 --- /dev/null +++ b/ressult/gui/material_calculator.py @@ -0,0 +1,160 @@ +# gui/material_calculator.py +""" +Калькулятор материалов для производства +Соответствует модулю 4 ТЗ +""" +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QMessageBox, QFormLayout, + QDoubleSpinBox, QSpinBox) +from PyQt6.QtCore import Qt +import requests +import math + +class MaterialCalculatorWindow(QDialog): + def __init__(self, parent=None, auth=None): + super().__init__(parent) + self.auth = auth # Сохраняем auth, даже если не используется + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Калькулятор материалов для производства") + self.setModal(True) + self.resize(400, 300) + + layout = QVBoxLayout() + + # Заголовок + title = QLabel("Расчет количества материала") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;") + layout.addWidget(title) + + # Форма ввода параметров + form_layout = QFormLayout() + + self.product_type_id = QSpinBox() + self.product_type_id.setRange(1, 100) + form_layout.addRow("ID типа продукции:", self.product_type_id) + + self.material_type_id = QSpinBox() + self.material_type_id.setRange(1, 100) + form_layout.addRow("ID типа материала:", self.material_type_id) + + self.quantity = QSpinBox() + self.quantity.setRange(1, 1000000) + form_layout.addRow("Количество продукции:", self.quantity) + + self.param1 = QDoubleSpinBox() + self.param1.setRange(0.1, 1000.0) + self.param1.setDecimals(2) + form_layout.addRow("Параметр продукции 1:", self.param1) + + self.param2 = QDoubleSpinBox() + self.param2.setRange(0.1, 1000.0) + self.param2.setDecimals(2) + form_layout.addRow("Параметр продукции 2:", self.param2) + + self.product_coeff = QDoubleSpinBox() + self.product_coeff.setRange(0.1, 10.0) + self.product_coeff.setDecimals(3) + self.product_coeff.setValue(1.0) + form_layout.addRow("Коэффициент типа продукции:", self.product_coeff) + + self.defect_percent = QDoubleSpinBox() + self.defect_percent.setRange(0.0, 50.0) + self.defect_percent.setDecimals(1) + self.defect_percent.setSuffix("%") + form_layout.addRow("Процент брака материала:", self.defect_percent) + + layout.addLayout(form_layout) + + # Результат + self.result_label = QLabel() + self.result_label.setStyleSheet("font-weight: bold; color: #007acc; margin: 10px;") + self.result_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.result_label) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.calculate_button = QPushButton("Рассчитать") + self.calculate_button.clicked.connect(self.calculate_material) + self.calculate_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.close_button = QPushButton("Закрыть") + self.close_button.clicked.connect(self.accept) + + buttons_layout.addWidget(self.calculate_button) + buttons_layout.addStretch() + buttons_layout.addWidget(self.close_button) + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + def calculate_material(self): + """Расчет количества материала с обработкой ошибок""" + try: + # Проверяем валидность входных данных + if (self.param1.value() <= 0 or self.param2.value() <= 0 or + self.product_coeff.value() <= 0 or self.defect_percent.value() < 0): + self.result_label.setText("Ошибка: неверные параметры") + self.result_label.setStyleSheet("color: red; font-weight: bold;") + return + + # Создаем данные для расчета + calculation_data = { + 'product_type_id': self.product_type_id.value(), + 'material_type_id': self.material_type_id.value(), + 'quantity': self.quantity.value(), + 'param1': self.param1.value(), + 'param2': self.param2.value(), + 'product_coeff': self.product_coeff.value(), + 'defect_percent': self.defect_percent.value() + } + + # Локальный расчет (без API) + material_quantity = self.calculate_locally(calculation_data) + + if material_quantity >= 0: + self.result_label.setText( + f"Необходимое количество материала: {material_quantity} единиц" + ) + self.result_label.setStyleSheet("color: #007acc; font-weight: bold;") + else: + self.result_label.setText("Ошибка: неверные параметры расчета") + self.result_label.setStyleSheet("color: red; font-weight: bold;") + + except Exception as e: + self.result_label.setText(f"Ошибка расчета: {str(e)}") + self.result_label.setStyleSheet("color: red; font-weight: bold;") + + def calculate_locally(self, data): + """Локальный расчет материалов""" + try: + import math + + # Расчет количества материала на одну единицу продукции + material_per_unit = data['param1'] * data['param2'] * data['product_coeff'] + + # Расчет общего количества материала с учетом брака + total_material = material_per_unit * data['quantity'] + total_material_with_defect = total_material * (1 + data['defect_percent'] / 100) + + # Округление до целого числа в большую сторону + return math.ceil(total_material_with_defect) + + except: + return -1 diff --git a/ressult/gui/orders_panel.py b/ressult/gui/orders_panel.py new file mode 100644 index 0000000..64576a3 --- /dev/null +++ b/ressult/gui/orders_panel.py @@ -0,0 +1,344 @@ +# gui/orders_panel.py +""" +Панель управления заказами и продажами +""" +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QTableWidget, QTableWidgetItem, QPushButton, + QHeaderView, QMessageBox, QDateEdit, QComboBox, + QLineEdit, QFormLayout, QDialog, QDoubleSpinBox) +from PyQt6.QtCore import Qt, QDate +from PyQt6.QtGui import QFont +import requests + +class OrderForm(QDialog): + """Форма для добавления/редактирования заказа""" + + order_saved = pyqtSignal() + + def __init__(self, parent=None, order_data=None, auth=None, partners=None): + super().__init__(parent) + self.order_data = order_data + self.auth = auth + self.partners = partners or [] + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Добавить заказ" if not self.order_data else "Редактировать заказ") + self.setModal(True) + self.resize(400, 300) + + layout = QVBoxLayout() + + # Форма ввода данных + form_layout = QFormLayout() + + # Выбор партнера + self.partner_combo = QComboBox() + self.partner_combo.addItem("Выберите партнера", None) + for partner in self.partners: + self.partner_combo.addItem(partner['company_name'], partner['partner_id']) + form_layout.addRow("Партнер*:", self.partner_combo) + + # Название продукта + self.product_name = QLineEdit() + self.product_name.setPlaceholderText("Введите название продукта") + form_layout.addRow("Продукт*:", self.product_name) + + # Количество + self.quantity = QDoubleSpinBox() + self.quantity.setRange(0.01, 100000.0) + self.quantity.setDecimals(2) + form_layout.addRow("Количество*:", self.quantity) + + # Дата продажи + self.sale_date = QDateEdit() + self.sale_date.setDate(QDate.currentDate()) + self.sale_date.setCalendarPopup(True) + form_layout.addRow("Дата продажи*:", self.sale_date) + + layout.addLayout(form_layout) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.save_button = QPushButton("Сохранить") + self.save_button.clicked.connect(self.save_order) + self.save_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + + self.cancel_button = QPushButton("Отмена") + self.cancel_button.clicked.connect(self.reject) + + buttons_layout.addWidget(self.save_button) + buttons_layout.addWidget(self.cancel_button) + buttons_layout.addStretch() + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + # Если редактирование, заполняем форму + if self.order_data: + self.fill_form() + + def fill_form(self): + """Заполнение формы данными заказа""" + data = self.order_data + + # Устанавливаем партнера + partner_index = self.partner_combo.findData(data.get('partner_id')) + if partner_index >= 0: + self.partner_combo.setCurrentIndex(partner_index) + + self.product_name.setText(data.get('product_name', '')) + self.quantity.setValue(float(data.get('quantity', 0))) + + # Устанавливаем дату + sale_date = data.get('sale_date') + if sale_date: + date = QDate.fromString(sale_date, 'yyyy-MM-dd') + if date.isValid(): + self.sale_date.setDate(date) + + def validate_form(self): + """Валидация данных формы""" + errors = [] + + if not self.partner_combo.currentData(): + errors.append("Выберите партнера") + + if not self.product_name.text().strip(): + errors.append("Введите название продукта") + + if self.quantity.value() <= 0: + errors.append("Количество должно быть больше 0") + + return errors + + def save_order(self): + """Сохранение заказа""" + errors = self.validate_form() + if errors: + QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors)) + return + + order_data = { + 'partner_id': self.partner_combo.currentData(), + 'product_name': self.product_name.text().strip(), + 'quantity': self.quantity.value(), + 'sale_date': self.sale_date.date().toString('yyyy-MM-dd') + } + + try: + if self.order_data: + # Обновление существующего заказа + response = requests.put( + f"http://localhost:8000/api/v1/sales/{self.order_data['sale_id']}", + json=order_data, + auth=self.auth, + timeout=10 + ) + else: + # Создание нового заказа + response = requests.post( + "http://localhost:8000/api/v1/sales", + json=order_data, + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.order_saved.emit() + QMessageBox.information(self, "Успех", "Заказ успешно сохранен") + self.accept() + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + else: + error_msg = response.json().get('detail', 'Неизвестная ошибка') + QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить заказ: {error_msg}") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}") + +class OrdersPanel(QWidget): + """Панель управления заказами""" + + def __init__(self, auth=None): + super().__init__() + self.auth = auth + self.partners = [] + self.setup_ui() + self.load_partners() + self.load_orders() + + def setup_ui(self): + """Настройка интерфейса панели заказов""" + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок + title = QLabel("Управление заказами") + title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(title) + + # Панель управления + control_layout = QHBoxLayout() + + self.add_button = QPushButton("Добавить заказ") + self.add_button.clicked.connect(self.show_add_order_form) + self.add_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.refresh_button = QPushButton("Обновить") + self.refresh_button.clicked.connect(self.load_orders) + + control_layout.addWidget(self.add_button) + control_layout.addWidget(self.refresh_button) + control_layout.addStretch() + + layout.addLayout(control_layout) + + # Таблица заказов + self.orders_table = QTableWidget() + self.orders_table.setColumnCount(6) + self.orders_table.setHorizontalHeaderLabels([ + "ID", "Партнер", "Продукт", "Количество", "Дата", "Действия" + ]) + self.orders_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + self.orders_table.setStyleSheet(""" + QTableWidget { + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + } + QTableWidget::item { + padding: 8px; + } + """) + + layout.addWidget(self.orders_table) + + self.setLayout(layout) + + def load_partners(self): + """Загрузка списка партнеров""" + try: + response = requests.get( + "http://localhost:8000/api/v1/partners", + auth=self.auth, + timeout=10 + ) + if response.status_code == 200: + self.partners = response.json() + except: + self.partners = [] + + def load_orders(self): + """Загрузка списка заказов""" + try: + response = requests.get( + "http://localhost:8000/api/v1/sales", + auth=self.auth, + timeout=10 + ) + if response.status_code == 200: + orders = response.json() + self.display_orders(orders) + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить заказы: {str(e)}") + + def display_orders(self, orders): + """Отображение заказов в таблице""" + self.orders_table.setRowCount(len(orders)) + + for row, order in enumerate(orders): + self.orders_table.setItem(row, 0, QTableWidgetItem(str(order.get('sale_id', '')))) + self.orders_table.setItem(row, 1, QTableWidgetItem(order.get('company_name', 'Неизвестно'))) + self.orders_table.setItem(row, 2, QTableWidgetItem(order.get('product_name', ''))) + self.orders_table.setItem(row, 3, QTableWidgetItem(str(order.get('quantity', '')))) + self.orders_table.setItem(row, 4, QTableWidgetItem(order.get('sale_date', ''))) + + # Кнопки действий + actions_widget = QWidget() + actions_layout = QHBoxLayout(actions_widget) + actions_layout.setContentsMargins(4, 4, 4, 4) + actions_layout.setSpacing(4) + + delete_button = QPushButton("Удалить") + delete_button.setStyleSheet(""" + QPushButton { + background-color: #dc3545; + color: white; + border: none; + padding: 4px 8px; + border-radius: 3px; + font-size: 11px; + } + QPushButton:hover { + background-color: #c82333; + } + """) + delete_button.clicked.connect(lambda checked, o=order: self.delete_order(o)) + + actions_layout.addWidget(delete_button) + actions_layout.addStretch() + + self.orders_table.setCellWidget(row, 5, actions_widget) + + def show_add_order_form(self): + """Открытие формы добавления заказа""" + form = OrderForm(self, auth=self.auth, partners=self.partners) + form.order_saved.connect(self.load_orders) + form.exec() + + def delete_order(self, order): + """Удаление заказа""" + reply = QMessageBox.question( + self, + "Подтверждение удаления", + f"Вы уверены, что хотите удалить заказ #{order.get('sale_id')}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + try: + response = requests.delete( + f"http://localhost:8000/api/v1/sales/{order['sale_id']}", + auth=self.auth, + timeout=10 + ) + if response.status_code == 200: + self.load_orders() + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось удалить заказ: {str(e)}") diff --git a/ressult/gui/partner_form.py b/ressult/gui/partner_form.py new file mode 100644 index 0000000..9d2310b --- /dev/null +++ b/ressult/gui/partner_form.py @@ -0,0 +1,193 @@ +# gui/partner_form.py (обновленный) +""" +Форма для добавления/редактирования партнера с поддержкой авторизации +""" +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QComboBox, QPushButton, QMessageBox, + QFormLayout, QSpinBox) +from PyQt6.QtCore import pyqtSignal +import requests + +class PartnerForm(QDialog): + partner_saved = pyqtSignal() + + def __init__(self, parent=None, partner_data=None, auth=None): + super().__init__(parent) + self.partner_data = partner_data + self.auth = auth + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Добавить партнера" if not self.partner_data else "Редактировать партнера") + self.setModal(True) + self.resize(500, 400) + + layout = QVBoxLayout() + + # Форма ввода данных + form_layout = QFormLayout() + + self.company_name = QLineEdit() + self.company_name.setPlaceholderText("Введите наименование компании") + form_layout.addRow("Наименование компании*:", self.company_name) + + self.inn = QLineEdit() + self.inn.setPlaceholderText("Введите ИНН") + form_layout.addRow("ИНН*:", self.inn) + + self.partner_type = QComboBox() + self.partner_type.addItems(["", "distributor", "retail", "wholesale", "dealer"]) + self.partner_type.setPlaceholderText("Выберите тип партнера") + form_layout.addRow("Тип партнера:", self.partner_type) + + self.rating = QSpinBox() + self.rating.setRange(0, 100) + self.rating.setSuffix("%") + form_layout.addRow("Рейтинг:", self.rating) + + self.legal_address = QLineEdit() + self.legal_address.setPlaceholderText("Введите юридический адрес") + form_layout.addRow("Юридический адрес:", self.legal_address) + + self.director_name = QLineEdit() + self.director_name.setPlaceholderText("Введите ФИО директора") + form_layout.addRow("ФИО директора:", self.director_name) + + self.phone = QLineEdit() + self.phone.setPlaceholderText("+7XXXXXXXXXX") + form_layout.addRow("Телефон:", self.phone) + + self.email = QLineEdit() + self.email.setPlaceholderText("email@example.com") + form_layout.addRow("Email:", self.email) + + self.sales_locations = QLineEdit() + self.sales_locations.setPlaceholderText("Москва, Санкт-Петербург...") + form_layout.addRow("Регионы продаж:", self.sales_locations) + + layout.addLayout(form_layout) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.save_button = QPushButton("Сохранить") + self.save_button.clicked.connect(self.save_partner) + self.save_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + + self.cancel_button = QPushButton("Отмена") + self.cancel_button.clicked.connect(self.reject) + + buttons_layout.addWidget(self.save_button) + buttons_layout.addWidget(self.cancel_button) + buttons_layout.addStretch() + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + # Если редактирование, заполняем форму + if self.partner_data: + self.fill_form() + + def fill_form(self): + """Заполнение формы данными партнера""" + data = self.partner_data + self.company_name.setText(data.get('company_name', '')) + self.inn.setText(data.get('inn', '')) + + partner_type = data.get('partner_type', '') + if partner_type: + index = self.partner_type.findText(partner_type) + if index >= 0: + self.partner_type.setCurrentIndex(index) + + # Безопасное преобразование рейтинга + rating = data.get('rating', 0) + if isinstance(rating, float): + rating = int(rating) + self.rating.setValue(rating) + + self.legal_address.setText(data.get('legal_address', '')) + self.director_name.setText(data.get('director_name', '')) + self.phone.setText(data.get('phone', '')) + self.email.setText(data.get('email', '')) + self.sales_locations.setText(data.get('sales_locations', '')) + + def validate_form(self): + """Валидация данных формы""" + errors = [] + + if not self.company_name.text().strip(): + errors.append("Наименование компании обязательно") + + if not self.inn.text().strip(): + errors.append("ИНН обязателен") + + if self.phone.text() and not self.phone.text().startswith('+'): + errors.append("Телефон должен начинаться с '+'") + + return errors + + def save_partner(self): + """Сохранение партнера с авторизацией""" + errors = self.validate_form() + if errors: + QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors)) + return + + partner_data = { + 'company_name': self.company_name.text().strip(), + 'inn': self.inn.text().strip(), + 'partner_type': self.partner_type.currentText() or None, + 'rating': self.rating.value(), + 'legal_address': self.legal_address.text().strip() or None, + 'director_name': self.director_name.text().strip() or None, + 'phone': self.phone.text().strip() or None, + 'email': self.email.text().strip() or None, + 'sales_locations': self.sales_locations.text().strip() or None + } + + try: + if self.partner_data: + # Обновление существующего партнера + response = requests.put( + f"http://localhost:8000/api/v1/partners/{self.partner_data['partner_id']}", + json=partner_data, + auth=self.auth, + timeout=10 + ) + else: + # Создание нового партнера + response = requests.post( + "http://localhost:8000/api/v1/partners", + json=partner_data, + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.partner_saved.emit() + QMessageBox.information(self, "Успех", "Партнер успешно сохранен") + self.accept() + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + else: + error_msg = response.json().get('detail', 'Неизвестная ошибка') + QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить партнера: {error_msg}") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}") diff --git a/ressult/gui/partner_form.py.bak b/ressult/gui/partner_form.py.bak new file mode 100644 index 0000000..da98b84 --- /dev/null +++ b/ressult/gui/partner_form.py.bak @@ -0,0 +1,186 @@ +# gui/partner_form.py +""" +Форма для добавления/редактирования партнера +Соответствует модулю 3 ТЗ +""" +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QComboBox, QPushButton, QMessageBox, + QFormLayout, QSpinBox) +from PyQt6.QtCore import pyqtSignal +import requests + +class PartnerForm(QDialog): + partner_saved = pyqtSignal() + + def __init__(self, parent=None, partner_data=None): + super().__init__(parent) + self.partner_data = partner_data + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Добавить партнера" if not self.partner_data else "Редактировать партнера") + self.setModal(True) + self.resize(500, 400) + + layout = QVBoxLayout() + + # Форма ввода данных + form_layout = QFormLayout() + + self.company_name = QLineEdit() + self.company_name.setPlaceholderText("Введите наименование компании") + form_layout.addRow("Наименование компании*:", self.company_name) + + self.inn = QLineEdit() + self.inn.setPlaceholderText("Введите ИНН") + form_layout.addRow("ИНН*:", self.inn) + + self.partner_type = QComboBox() + self.partner_type.addItems(["", "distributor", "retail", "wholesale", "dealer"]) + self.partner_type.setPlaceholderText("Выберите тип партнера") + form_layout.addRow("Тип партнера:", self.partner_type) + + self.rating = QSpinBox() + self.rating.setRange(0, 100) + self.rating.setSuffix("%") + form_layout.addRow("Рейтинг:", self.rating) + + self.legal_address = QLineEdit() + self.legal_address.setPlaceholderText("Введите юридический адрес") + form_layout.addRow("Юридический адрес:", self.legal_address) + + self.director_name = QLineEdit() + self.director_name.setPlaceholderText("Введите ФИО директора") + form_layout.addRow("ФИО директора:", self.director_name) + + self.phone = QLineEdit() + self.phone.setPlaceholderText("+7XXXXXXXXXX") + form_layout.addRow("Телефон:", self.phone) + + self.email = QLineEdit() + self.email.setPlaceholderText("email@example.com") + form_layout.addRow("Email:", self.email) + + self.sales_locations = QLineEdit() + self.sales_locations.setPlaceholderText("Москва, Санкт-Петербург...") + form_layout.addRow("Регионы продаж:", self.sales_locations) + + layout.addLayout(form_layout) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.save_button = QPushButton("Сохранить") + self.save_button.clicked.connect(self.save_partner) + self.save_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + + self.cancel_button = QPushButton("Отмена") + self.cancel_button.clicked.connect(self.reject) + + buttons_layout.addWidget(self.save_button) + buttons_layout.addWidget(self.cancel_button) + buttons_layout.addStretch() + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + # Если редактирование, заполняем форму + if self.partner_data: + self.fill_form() + + # gui/partner_form.py (исправленный метод fill_form) + def fill_form(self): + """Заполнение формы данными партнера""" + data = self.partner_data + self.company_name.setText(data.get('company_name', '')) + self.inn.setText(data.get('inn', '')) + + partner_type = data.get('partner_type', '') + if partner_type: + index = self.partner_type.findText(partner_type) + if index >= 0: + self.partner_type.setCurrentIndex(index) + + # Безопасное преобразование рейтинга к int + rating = data.get('rating', 0) + if isinstance(rating, float): + rating = int(rating) + self.rating.setValue(rating) + + self.legal_address.setText(data.get('legal_address', '')) + self.director_name.setText(data.get('director_name', '')) + self.phone.setText(data.get('phone', '')) + self.email.setText(data.get('email', '')) + self.sales_locations.setText(data.get('sales_locations', '')) + + def validate_form(self): + """Валидация данных формы""" + errors = [] + + if not self.company_name.text().strip(): + errors.append("Наименование компании обязательно") + + if not self.inn.text().strip(): + errors.append("ИНН обязателен") + + if self.phone.text() and not self.phone.text().startswith('+'): + errors.append("Телефон должен начинаться с '+'") + + return errors + + def save_partner(self): + """Сохранение партнера""" + errors = self.validate_form() + if errors: + QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors)) + return + + partner_data = { + 'company_name': self.company_name.text().strip(), + 'inn': self.inn.text().strip(), + 'partner_type': self.partner_type.currentText() or None, + 'rating': self.rating.value(), + 'legal_address': self.legal_address.text().strip() or None, + 'director_name': self.director_name.text().strip() or None, + 'phone': self.phone.text().strip() or None, + 'email': self.email.text().strip() or None, + 'sales_locations': self.sales_locations.text().strip() or None + } + + try: + if self.partner_data: + # Обновление существующего партнера + response = requests.put( + f"http://localhost:8000/api/v1/partners/{self.partner_data['partner_id']}", + json=partner_data + ) + else: + # Создание нового партнера + response = requests.post( + "http://localhost:8000/api/v1/partners", + json=partner_data + ) + + if response.status_code == 200: + self.partner_saved.emit() + QMessageBox.information(self, "Успех", "Партнер успешно сохранен") + self.accept() + else: + error_msg = response.json().get('detail', 'Неизвестная ошибка') + QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить партнера: {error_msg}") + + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}") diff --git a/ressult/gui/sales_history.py b/ressult/gui/sales_history.py new file mode 100644 index 0000000..1c4c571 --- /dev/null +++ b/ressult/gui/sales_history.py @@ -0,0 +1,91 @@ +# gui/sales_history.py +""" +Окно истории продаж партнера +Соответствует модулю 4 ТЗ +""" +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QTableWidget, QTableWidgetItem, QPushButton, + QHeaderView, QMessageBox) +from PyQt6.QtCore import Qt +import requests + +class SalesHistoryWindow(QDialog): + def __init__(self, partner_data, parent=None): + super().__init__(parent) + self.partner_data = partner_data + self.setup_ui() + self.load_sales_history() + + def setup_ui(self): + self.setWindowTitle(f"История продаж - {self.partner_data['company_name']}") + self.setModal(True) + self.resize(800, 400) + + layout = QVBoxLayout() + + # Заголовок + title = QLabel(f"История реализации продукции\n{self.partner_data['company_name']}") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;") + layout.addWidget(title) + + # Таблица продаж + self.sales_table = QTableWidget() + self.sales_table.setColumnCount(4) + self.sales_table.setHorizontalHeaderLabels([ + "ID", "Наименование продукции", "Количество", "Дата продажи" + ]) + self.sales_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + layout.addWidget(self.sales_table) + + # Статистика + self.stats_label = QLabel() + self.stats_label.setStyleSheet("font-weight: bold; margin: 10px;") + layout.addWidget(self.stats_label) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.close_button = QPushButton("Закрыть") + self.close_button.clicked.connect(self.accept) + + buttons_layout.addStretch() + buttons_layout.addWidget(self.close_button) + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + def load_sales_history(self): + """Загрузка истории продаж партнера""" + try: + response = requests.get( + f"http://localhost:8000/api/v1/sales/partner/{self.partner_data['partner_id']}" + ) + if response.status_code == 200: + sales_data = response.json() + self.display_sales_data(sales_data) + else: + QMessageBox.warning(self, "Ошибка", "Не удалось загрузить историю продаж") + + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}") + + def display_sales_data(self, sales_data): + """Отображение данных о продажах в таблице""" + self.sales_table.setRowCount(len(sales_data)) + + total_quantity = 0 + for row, sale in enumerate(sales_data): + self.sales_table.setItem(row, 0, QTableWidgetItem(str(sale['sale_id']))) + self.sales_table.setItem(row, 1, QTableWidgetItem(sale['product_name'])) + self.sales_table.setItem(row, 2, QTableWidgetItem(str(sale['quantity']))) + self.sales_table.setItem(row, 3, QTableWidgetItem(sale['sale_date'])) + + total_quantity += float(sale['quantity']) + + # Обновление статистики + self.stats_label.setText( + f"Общее количество проданной продукции: {total_quantity}\n" + f"Всего продаж: {len(sales_data)}" + ) diff --git a/ressult/requirements.txt b/ressult/requirements.txt new file mode 100644 index 0000000..ace2609 --- /dev/null +++ b/ressult/requirements.txt @@ -0,0 +1,11 @@ +# requirements.txt +fastapi==0.104.1 +uvicorn==0.24.0 +psycopg2-binary==2.9.9 +python-dotenv==1.0.0 +python-multipart==0.0.6 +pandas==2.1.3 +openpyxl==3.1.2 +aiofiles==23.2.1 +pydantic[email]==2.5.0 +bcrypt==4.1.1 diff --git a/ressult/run.py b/ressult/run.py new file mode 100644 index 0000000..8bddcfa --- /dev/null +++ b/ressult/run.py @@ -0,0 +1,17 @@ +# run.py +""" +Точка входа для запуска сервера +""" +import uvicorn +import os +from dotenv import load_dotenv + +load_dotenv() + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host=os.getenv('HOST', '0.0.0.0'), + port=int(os.getenv('PORT', 8000)), + reload=os.getenv('DEBUG', 'False').lower() == 'true' + ) diff --git a/ressult/run_gui.py b/ressult/run_gui.py new file mode 100644 index 0000000..aacd95d --- /dev/null +++ b/ressult/run_gui.py @@ -0,0 +1,51 @@ +# run_gui.py +""" +Главный модуль запуска GUI приложения с авторизацией +""" +import sys +import os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from gui.login_window import LoginWindow +from gui.main_window import MainWindow +from PyQt6.QtWidgets import QApplication +from PyQt6.QtCore import QTimer + +class ApplicationController: + """Контроллер приложения, управляющий авторизацией и главным окном""" + + def __init__(self): + self.app = QApplication(sys.argv) + self.login_window = None + self.main_window = None + self.current_user = None + + def show_login(self): + """Показать окно авторизации""" + self.login_window = LoginWindow() + self.login_window.login_success.connect(self.on_login_success) + self.login_window.show() + + def on_login_success(self, user_data): + """Обработка успешной авторизации""" + self.current_user = user_data + self.login_window.close() + self.show_main_window() + + def show_main_window(self): + """Показать главное окно приложения""" + self.main_window = MainWindow(self.current_user) + self.main_window.show() + + def run(self): + """Запуск приложения""" + self.show_login() + return self.app.exec() + +def main(): + """Точка входа приложения""" + controller = ApplicationController() + sys.exit(controller.run()) + +if __name__ == "__main__": + main() diff --git a/robbery/master_pol-module_1_2/.gitignore b/robbery/master_pol-module_1_2/.gitignore new file mode 100644 index 0000000..eb7063d --- /dev/null +++ b/robbery/master_pol-module_1_2/.gitignore @@ -0,0 +1,2 @@ +**/__pycache__/ +.venv/ \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/.gitignore b/robbery/master_pol-module_1_2/.idea/.gitignore new file mode 100644 index 0000000..eaf91e2 --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/robbery/master_pol-module_1_2/.idea/.name b/robbery/master_pol-module_1_2/.idea/.name new file mode 100644 index 0000000..11a5d8e --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/.name @@ -0,0 +1 @@ +main.py \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml b/robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml b/robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml new file mode 100644 index 0000000..9bd607d --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/misc.xml b/robbery/master_pol-module_1_2/.idea/misc.xml new file mode 100644 index 0000000..953f9db --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/modules.xml b/robbery/master_pol-module_1_2/.idea/modules.xml new file mode 100644 index 0000000..f8a1763 --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/README.md b/robbery/master_pol-module_1_2/README.md new file mode 100644 index 0000000..1c67087 --- /dev/null +++ b/robbery/master_pol-module_1_2/README.md @@ -0,0 +1,55 @@ +# MasterPol + +Графическое приложение на PyQt6 для работы с базой данных MySQL. + +## Подготовка проекта + +1. **Клонируйте репозиторий и перейдите в папку проекта:** + + ```sh + git clone <адрес-репозитория> + cd master_pol + ``` + +2. **Создайте и активируйте виртуальное окружение:** + + ```sh + python -m venv .venv + .venv\Scripts\activate # Windows + # source .venv/bin/activate # Linux/MacOS + ``` + +3. **Установите зависимости:** + + ```sh + pip install -r requirements.txt + ``` + +4. **Создайте базу данных и выполните SQL-скрипт:** + + - Запустите MySQL и выполните скрипт `app/database/script.sql` для создания необходимых таблиц и данных: + + ```sh + mysql -u -p < app/database/script.sql + ``` + + - Замените `` и `` на свои значения. + +5. **Проверьте параметры подключения к базе данных:** + - Откройте файл `app/database/db.py` и убедитесь, что значения для подключения (host, user, password, database) указаны верно. + +## Запуск приложения + +```sh +python app/main.py +``` + +## Структура проекта + +- `app/main.py` — точка входа, запуск приложения +- `app/components/` — компоненты интерфейса +- `app/database/` — работа с БД, скрипты и настройки +- `app/pages/` — страницы приложения +- `app/res/` — ресурсы (цвета, шрифты) + +--- diff --git a/robbery/master_pol-module_1_2/app/components/edit_partner_dialog.py b/robbery/master_pol-module_1_2/app/components/edit_partner_dialog.py new file mode 100644 index 0000000..f596812 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/components/edit_partner_dialog.py @@ -0,0 +1,108 @@ +from PyQt6.QtWidgets import ( + QDialog, + QVBoxLayout, + QFormLayout, + QLineEdit, + QPushButton, + QComboBox, + QSpinBox, + QMessageBox, +) +from PyQt6.QtCore import Qt +from res.colors import ACCENT_COLOR +from dto.partners_dto import PartnerUpdateDto, PartnersInfo + + +class EditPartnerDialog(QDialog): + def __init__(self, partner_data: PartnersInfo, parent=None): + super().__init__(parent) + self.partner_data = partner_data + self.setup_ui() + self.load_partner_types() + self.fill_form() + self.result = None + + def setup_ui(self): + self.setWindowTitle("Редактирование партнера") + self.setFixedSize(500, 400) + + layout = QVBoxLayout() + form_layout = QFormLayout() + + # Создаем поля формы + self.partner_type = QComboBox() + self.partner_name = QLineEdit() + self.first_name = QLineEdit() + self.last_name = QLineEdit() + self.middle_name = QLineEdit() + self.email = QLineEdit() + self.phone = QLineEdit() + self.address = QLineEdit() + self.inn = QLineEdit() + self.rating = QSpinBox() + self.rating.setRange(0, 10) + + # Добавляем поля в форму + form_layout.addRow("Тип партнера:", self.partner_type) + form_layout.addRow("Название:", self.partner_name) + form_layout.addRow("Имя директора:", self.first_name) + form_layout.addRow("Фамилия директора:", self.last_name) + form_layout.addRow("Отчество директора:", self.middle_name) + form_layout.addRow("Email:", self.email) + form_layout.addRow("Телефон:", self.phone) + form_layout.addRow("Адрес:", self.address) + form_layout.addRow("ИНН:", self.inn) + form_layout.addRow("Рейтинг:", self.rating) + + # Кнопки + self.save_button = QPushButton("Сохранить") + self.cancel_button = QPushButton("Отмена") + + self.save_button.clicked.connect(self.save_changes) + self.cancel_button.clicked.connect(self.reject) + + layout.addLayout(form_layout) + layout.addWidget(self.save_button) + layout.addWidget(self.cancel_button) + + self.setLayout(layout) + + # Стили + self.setStyleSheet( + f""" + QPushButton {{ + background-color: {ACCENT_COLOR}; + padding: 8px; + border-radius: 4px; + }} + """ + ) + + def load_partner_types(self): + types = ['ООО', "ЗАО"] + for i, val in enumerate(types): + self.partner_type.addItem(val, i + 1) + + def fill_form(self): + pass + def save_changes(self): + try: + partner_data = PartnerUpdateDto( + id=self.partner_data.id, + partner_type_id=self.partner_type.currentData(), + partner_name=self.partner_name.text(), + first_name=self.first_name.text(), + last_name=self.last_name.text(), + middle_name=self.middle_name.text(), + email=self.email.text(), + phone=self.phone.text(), + address=self.address.text(), + inn=self.inn.text(), + rating=self.rating.value(), + ) + db.update_partner(partner_data) + self.accept() + except Exception as e: + QMessageBox.critical( + self, "Ошибка", f"Не удалось сохранить изменения: {str(e)}" + ) diff --git a/robbery/master_pol-module_1_2/app/components/partner_card.py b/robbery/master_pol-module_1_2/app/components/partner_card.py new file mode 100644 index 0000000..8b462a3 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/components/partner_card.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass +from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QFrame +from PyQt6.QtCore import Qt, pyqtSignal +from res.colors import ACCENT_COLOR, SECONDARY_COLOR +from res.fonts import MAIN_FONT +from dto.partners_dto import PartnersInfo + + + + +class PartnerCard(QFrame): + doubleClicked = pyqtSignal(PartnersInfo) + + def __init__(self, info: PartnersInfo): + super().__init__() + self.info = info + + self.init_ui() + self.set_styles() + + def mouseDoubleClickEvent(self, a0): + self.doubleClicked.emit(self.info) + return super().mouseDoubleClickEvent(a0) + + def init_ui(self): + main_layout = QVBoxLayout() + self.setLayout(main_layout) + + # Верхняя строка: Тип | Наименование и скидка + header_layout = QHBoxLayout() + header_text = QLabel(f"{self.info.type_name} | {self.info.partner_name}") + header_text.setObjectName("partnerHeader") + discount_text = QLabel(f"{self.info.discount}%") + discount_text.setObjectName("partnerDiscount") + + header_layout.addWidget(header_text) + header_layout.addWidget(discount_text, alignment=Qt.AlignmentFlag.AlignRight) + + # Информация о директоре + director_text = QLabel(f"Директор") + director_text.setObjectName("fieldLabel") + director_name = QLabel( + f"{self.info.last_name_director} {self.info.first_name_director} {self.info.middle_name_director}" + ) + + # Контактная информация + phone_text = QLabel(f"+{self.info.phone_partner}") + + # Рейтинг + rating_layout = QHBoxLayout() + rating_label = QLabel("Рейтинг:") + rating_label.setObjectName("fieldLabel") + rating_value = QLabel(str(self.info.rating)) + rating_layout.addWidget(rating_label) + rating_layout.addWidget(rating_value) + rating_layout.addStretch() + + # Добавляем все элементы в главный layout + main_layout.addLayout(header_layout) + main_layout.addWidget(director_text) + main_layout.addWidget(director_name) + main_layout.addWidget(phone_text) + main_layout.addLayout(rating_layout) + + def set_styles(self): + self.setStyleSheet( + """ + PartnerCard { + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px; + margin: 5px; + background-color: white; + } + QLabel { + font-family: %s; + } + #partnerHeader { + font-size: 18px; + font-weight: bold; + color: %s; + } + #partnerDiscount { + font-size: 18px; + font-weight: bold; + color: %s; + } + #fieldLabel { + color: gray; + font-size: 14px; + } + """ + % (MAIN_FONT, ACCENT_COLOR, SECONDARY_COLOR) + ) diff --git a/robbery/master_pol-module_1_2/app/database/db.py b/robbery/master_pol-module_1_2/app/database/db.py new file mode 100644 index 0000000..54590f0 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/database/db.py @@ -0,0 +1,84 @@ +import pymysql as psql +from dto.partners_dto import PartnerUpdateDto + + +class Database: + def __init__(self, host, user, password, db): + self.connection = psql.connect( + host=host, + user=user, + password=password, + database=db, + cursorclass=psql.cursors.DictCursor, + ) + + def authorize_user(self, username, password): + query = "SELECT * FROM users WHERE username=%s AND password=%s" + with self.connection.cursor() as cur: + cur.execute(query, (username, password)) + result = cur.fetchone() + return result is not None + + def execute_select(self, query, params=None): + """Выполняет SELECT запрос и возвращает результаты""" + with self.connection.cursor() as cur: + if params: + cur.execute(query, params) + else: + cur.execute(query) + return cur.fetchall() + + def get_partner_types(self): + """Получает все типы партнеров из таблицы partner_types""" + query = "SELECT * FROM partners_type" + with self.connection.cursor() as cur: + cur.execute(query) + return cur.fetchall() + + def update_partner(self, partners_info: PartnerUpdateDto): + with self.connection.cursor() as cur: + cur.callproc( + "upd_partner", + ( + partners_info.partner_type_id, + partners_info.id, + partners_info.partner_name, + partners_info.first_name, + partners_info.last_name, + partners_info.middle_name, + partners_info.email, + partners_info.phone, + partners_info.address, + partners_info.inn, + partners_info.rating, + ), + ) + self.connection.commit() + + def get_disc(self, partner_name): + """ + Получает скидку для партнера, вызывая функцию get_disc из БД + """ + # Сначала получим ID партнера по его имени + query = "SELECT id FROM partners WHERE partner_name = %s" + with self.connection.cursor() as cur: + cur.execute(query, (partner_name,)) + result = cur.fetchone() + + if not result: + return 0 + + # Вызываем функцию get_disc из БД + query = "SELECT get_disc(%s) as discount" + cur.execute(query, (result["id"],)) + discount_result = cur.fetchone() + + return discount_result["discount"] if discount_result else 0 + + +db = None +try: + db = Database(host="localhost", user="root", password="", db="master_pol") + print("Database connection established.") +except psql.MySQLError as e: + print(f"Error connecting to database: {e}") diff --git a/robbery/master_pol-module_1_2/app/database/script.sql b/robbery/master_pol-module_1_2/app/database/script.sql new file mode 100644 index 0000000..7d1b571 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/database/script.sql @@ -0,0 +1,460 @@ +CREATE DATABASE master_pol; +use master_pol; + +CREATE TABLE `partners` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `partner_type_id` INTEGER NOT NULL, + `partner_name` VARCHAR(255) NOT NULL, + `first_name_director` VARCHAR(50) NOT NULL, + `last_name_director` VARCHAR(50) NOT NULL, + `middle_name_director` VARCHAR(255), + `email_partner` VARCHAR(100) NOT NULL, + `phone_partner` VARCHAR(15) NOT NULL, + `address` VARCHAR(255) NOT NULL, + `INN` VARCHAR(10) NOT NULL, + `rating` INTEGER NOT NULL, + `logo` LONGBLOB, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `partners_type` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(255), + PRIMARY KEY(`id`) +); + + +CREATE TABLE `products` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `article` VARCHAR(10) NOT NULL, + `name` VARCHAR(100) NOT NULL, + `product_type_id` INTEGER NOT NULL, + `description` VARCHAR(255), + `picture` LONGBLOB, + `min_price_partners` DECIMAL(10,2) NOT NULL, + `cert_quality` LONGBLOB, + `standard_number` VARCHAR(255), + `selfcost` DECIMAL(10,2), + `length` DECIMAL(10,2), + `width` DECIMAL(10,2), + `height` DECIMAL(10,2), + `weight_no_package` DECIMAL(10,2), + `weight_with_package` DECIMAL(10,2), + `time_to_create_min` INTEGER, + `workshop_number` INTEGER, + `people_count_production` INTEGER, + `product_current_stock` INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `products_types` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(70) NOT NULL, + `coefficent` DECIMAL(3,2) NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `product_partners` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `product_id` INTEGER NOT NULL, + `partner_id` INTEGER NOT NULL, + `amount` INTEGER NOT NULL, + `sale_date` DATE NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `employees` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `employee_type_id` INTEGER NOT NULL, + `first_name` VARCHAR(50) NOT NULL, + `last_name` VARCHAR(50) NOT NULL, + `middle_name` VARCHAR(60) NULL, + `birth_date` DATE NOT NULL, + `passport_data` VARCHAR(11) NOT NULL, + `bank_details` VARCHAR(100) NOT NULL, + `has_family` BOOLEAN, + `health_status` VARCHAR(25), + PRIMARY KEY(`id`) +); + + +CREATE TABLE `employees_types` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(50) NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `users` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `username` VARCHAR(30) NOT NULL, + `password` VARCHAR(80) NOT NULL, + `employee_id` INTEGER NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `materials` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `material_type_id` INTEGER NOT NULL, + `supplier_id` INTEGER NOT NULL, + `name` VARCHAR(60) NOT NULL, + `package_quantity` INTEGER NOT NULL, + `unit` VARCHAR(20) NOT NULL, + `cost` DECIMAL(8,2) NOT NULL, + `image` LONGBLOB, + `min_stock` INTEGER, + `material_current_stock` INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `materials_type` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(50) NOT NULL, + `defect_percent` DECIMAL(10,2) NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `products_recipes` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `product_id` INTEGER NOT NULL, + `material_id` INTEGER NOT NULL, + `material_count` INTEGER NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `partners_rating_history` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `partner_id` INTEGER NOT NULL, + `new_rating` INTEGER NOT NULL, + `changed` DATETIME NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `orders` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `partner_id` INTEGER NOT NULL, + `manager_id` INTEGER NOT NULL, + `total_price` DECIMAL(10,2) NOT NULL, + `order_payment` DECIMAL(10,2) NOT NULL DEFAULT 0, + `created` DATETIME NOT NULL, + `status` ENUM('created', 'waiting prepayment', 'prepayment received', 'completed', 'canceled', 'ready for shipment', 'pending', 'in production') NOT NULL, + `prepayment_date` DATETIME, + `payment_date` DATETIME, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `products_orders` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `order_id` INTEGER NOT NULL, + `product_id` INTEGER NOT NULL, + `quantity` INTEGER NOT NULL, + `agreed_price_per` DECIMAL(8,2), + `production_date` DATE, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `suppliers` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(50) NOT NULL, + `INN` VARCHAR(10) NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `materials_supply_history` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `material_id` INTEGER NOT NULL, + `supplier_id` INTEGER NOT NULL, + `quantity` INTEGER NOT NULL, + `delivery_date` DATE NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `materials_movement` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `material_id` INTEGER NOT NULL, + `amount` INTEGER NOT NULL, + `movement_type` ENUM('incoming', 'reserve', 'write off') NOT NULL DEFAULT 'incoming', + `movement_date` DATETIME NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `employees_access` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `employee_id` INTEGER NOT NULL, + `door_id` INTEGER NOT NULL, + `access_date` DATETIME NOT NULL, + PRIMARY KEY(`id`) +); + + +ALTER TABLE `partners` +ADD FOREIGN KEY(`partner_type_id`) REFERENCES `partners_type`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products` +ADD FOREIGN KEY(`product_type_id`) REFERENCES `products_types`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `product_partners` +ADD FOREIGN KEY(`product_id`) REFERENCES `products`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `product_partners` +ADD FOREIGN KEY(`partner_id`) REFERENCES `partners`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `employees` +ADD FOREIGN KEY(`employee_type_id`) REFERENCES `employees_types`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `users` +ADD FOREIGN KEY(`employee_id`) REFERENCES `employees`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials` +ADD FOREIGN KEY(`material_type_id`) REFERENCES `materials_type`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products_recipes` +ADD FOREIGN KEY(`product_id`) REFERENCES `products`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products_recipes` +ADD FOREIGN KEY(`material_id`) REFERENCES `materials`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `partners_rating_history` +ADD FOREIGN KEY(`partner_id`) REFERENCES `partners`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `orders` +ADD FOREIGN KEY(`partner_id`) REFERENCES `partners`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `orders` +ADD FOREIGN KEY(`manager_id`) REFERENCES `employees`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products_orders` +ADD FOREIGN KEY(`order_id`) REFERENCES `orders`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products_orders` +ADD FOREIGN KEY(`product_id`) REFERENCES `products`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials` +ADD FOREIGN KEY(`supplier_id`) REFERENCES `suppliers`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials_supply_history` +ADD FOREIGN KEY(`material_id`) REFERENCES `materials`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials_supply_history` +ADD FOREIGN KEY(`supplier_id`) REFERENCES `suppliers`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials_movement` +ADD FOREIGN KEY(`material_id`) REFERENCES `materials`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `employees_access` +ADD FOREIGN KEY(`employee_id`) REFERENCES `employees`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; + +INSERT INTO materials_type (name, defect_percent) VALUES +('Тип материала 1', 0.001), +('Тип материала 2', 0.0095), +('Тип материала 3', 0.0028), +('Тип материала 4', 0.0055), +('Тип материала 5', 0.0034); + +INSERT INTO products_types (name, coefficent) VALUES +('Ламинат', 2.35), +('Массивная доска', 5.15), +('Паркетная доска', 4.34), +('Пробковое покрытие', 1.5); + +INSERT INTO partners_type (name) VALUES +('ЗАО'), +('ООО'), +('ПАО'), +('ОАО'); + + +INSERT INTO partners (partner_type_id, partner_name, first_name_director, last_name_director, middle_name_director, email_partner, phone_partner, address, INN, rating) VALUES +(1, 'База Строитель', 'Александра', 'Иванова', 'Ивановна', 'aleksandraivanova@ml.ru', '4931234567', '652050, Кемеровская область, город Юрга, ул. Лесная, 15', '2222455179', 7), +(2, 'Паркет 29', 'Василий', 'Петров', 'Петрович', 'vppetrov@vl.ru', '9871235678', '164500, Архангельская область, город Северодвинск, ул. Строителей, 18', '3333888520', 7), +(3, 'Стройсервис', 'Андрей', 'Соловьев', 'Николаевич', 'ansolovev@st.ru', '8122233200', '188910, Ленинградская область, город Приморск, ул. Парковая, 21', '4440391035', 7), +(4, 'Ремонт и отделка', 'Екатерина', 'Воробьева', 'Валерьевна', 'ekaterina.vorobeva@ml.ru', '4442223311', '143960, Московская область, город Реутов, ул. Свободы, 51', '1111520857', 5), +(1, 'МонтажПро', 'Степан', 'Степанов', 'Сергеевич', 'stepanov@stepan.ru', '9128883333', '309500, Белгородская область, город Старый Оскол, ул. Рабочая, 122', '5552431140', 10); + +INSERT INTO products (article, name, product_type_id, min_price_partners) VALUES +('8758385', 'Паркетная доска Ясень темный однополосная 14 мм', 3, 4456.90), +('8858958', 'Инженерная доска Дуб Французская елка однополосная 12 мм', 3, 7330.99), +('7750282', 'Ламинат Дуб дымчато-белый 33 класс 12 мм', 1, 1799.33), +('7028748', 'Ламинат Дуб серый 32 класс 8 мм с фаской', 1, 3890.41), +('5012543', 'Пробковое напольное клеевое покрытие 32 класс 4 мм', 4, 5450.59); + +INSERT INTO product_partners (product_id, partner_id, amount, sale_date) VALUES +(1, 1, 15500, '2023-03-23'), +(3, 1, 12350, '2023-12-18'), +(4, 1, 37400, '2024-06-07'), +(2, 2, 35000, '2022-12-02'), +(5, 2, 1250, '2023-05-17'), +(3, 2, 1000, '2024-06-07'), +(1, 2, 7550, '2024-07-01'), +(1, 3, 7250, '2023-01-22'), +(2, 3, 2500, '2024-07-05'), +(4, 4, 59050, '2023-03-20'), +(3, 4, 37200, '2024-03-12'), +(5, 4, 4500, '2024-05-14'), +(3, 5, 50000, '2023-09-19'), +(4, 5, 670000, '2023-11-10'), +(1, 5, 35000, '2024-04-15'), +(2, 5, 25000, '2024-06-12'); + +-- === 1. Типы сотрудников === +INSERT INTO employees_types (name) +VALUES +('Менеджер'), +('Бухгалтер'), +('Программист'), +('Охранник'), +('Уборщик'); + +-- === 2. Сотрудники === +INSERT INTO employees ( + employee_type_id, first_name, last_name, middle_name, birth_date, + passport_data, bank_details, has_family, health_status +) +VALUES +-- Менеджеры +(1, 'Иван', 'Петров', 'Сергеевич', '1988-03-15', '40051234567', '123456789', TRUE, 'Хорошее'), +(1, 'Мария', 'Сидорова', 'Игоревна', '1990-11-02', '40057891234', '987654321', FALSE, 'Отличное'), + +-- Программист +(3, 'Андрей', 'Кузнецов', 'Алексеевич', '1995-07-21', '40101234567', '111122223333', TRUE, 'Хорошее'), + +-- Бухгалтер +(2, 'Елена', 'Морозова', 'Павловна', '1982-05-08', '40104561234', '444455556666', TRUE, 'Удовлетворительное'), + +-- Охранник +(4, 'Сергей', 'Волков', 'Владимирович', '1979-09-10', '40205678901', '555566667777', FALSE, 'Хорошее'), + +-- Уборщик +(5, 'Наталья', 'Орлова', 'Геннадьевна', '1975-12-25', '40307891234', '888899990000', TRUE, 'Хорошее'); + +-- === 3. Пользователи === +-- Пользователи, связанные с менеджерами +INSERT INTO users (username, password, employee_id) +VALUES +('ivan', 'test', 1), +('manager_maria', 'hashed_password_456', 2); + + +CREATE VIEW show_partners +AS +SELECT p.id, pt.name AS type_name, p.partner_name, p.first_name_director, p.last_name_director, p.middle_name_director, p.phone_partner, p.rating +FROM partners p JOIN partners_type pt +ON +p.partner_type_id = pt.id; + + +DELIMITER // +CREATE PROCEDURE add_parther (IN p_partner_type_id INT, IN p_partner_name VARCHAR(255), +IN p_first_name_director VARCHAR(50), IN p_last_name_director VARCHAR(50), IN p_middle_name_director VARCHAR(255), +IN p_email_partner VARCHAR(100), IN p_phone_partner VARCHAR(15), IN p_address VARCHAR(255), IN p_INN VARCHAR(10), IN p_rating INT) + +BEGIN + INSERT INTO partners ( + partner_type_id, + partner_name, + first_name_director, + last_name_director, + middle_name_director, + email_partner, + phone_partner, + address, + INN, + rating + ) VALUES ( + p_partner_type_id, + p_partner_name, + p_first_name_director, + p_last_name_director, + p_middle_name_director, + p_email_partner, + p_phone_partner, + p_address, + p_INN, + p_rating + ); +END // + +DELIMITER ; + + +DELIMITER // + +CREATE PROCEDURE upd_partner (IN p_partner_type_id INT, IN p_id INT, IN p_partner_name VARCHAR(255), +IN p_first_name_director VARCHAR(50), IN p_last_name_director VARCHAR(50), IN p_middle_name_director VARCHAR(255), +IN p_email_partner VARCHAR(100), IN p_phone_partner VARCHAR(15), IN p_address VARCHAR(255), IN p_INN VARCHAR(10), IN p_rating INT) + +BEGIN + UPDATE partners + SET + partner_type_id = p_partner_type_id, + partner_name = p_partner_name, + first_name_director = p_first_name_director, + last_name_director = p_last_name_director, + middle_name_director = p_middle_name_director, + email_partner = p_email_partner, + phone_partner = p_phone_partner, + address = p_address, + INN = p_INN, + rating = p_rating + WHERE id = p_id; + +END // + +DELIMITER ; + + +DELIMITER // + +CREATE FUNCTION get_disc(partner_id INT) +RETURNS INT +BEGIN + + DECLARE total_amount INT; + + SELECT SUM(amount) INTO total_amount + FROM product_partners + WHERE partner_id = partner_id; + + IF total_amount >= 300000 THEN RETURN 15; + ELSEIF total_amount >= 50000 THEN RETURN 10; + ELSEIF total_amount >= 10000 THEN RETURN 5; + ELSE RETURN 0; + END IF; + +END // + +DELIMITER ; + + + +DELIMITER // + +CREATE PROCEDURE partner_history(IN p_partner_id INT) +BEGIN + SELECT + pr.name AS product_name, + pp.amount AS quantity, + pp.sale_date AS sale_date + FROM product_partners pp JOIN products pr + ON + pp.product_id = pr.id + WHERE pp.partner_id = p_partner_id + ORDER BY pp.sale_date DESC; +END// + +DELIMITER ; diff --git a/robbery/master_pol-module_1_2/app/dto/partners_dto.py b/robbery/master_pol-module_1_2/app/dto/partners_dto.py new file mode 100644 index 0000000..63b1107 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/dto/partners_dto.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass + + +@dataclass +class PartnersInfo: + id: int + type_name: str + partner_name: str + first_name_director: str + last_name_director: str + middle_name_director: str + phone_partner: str + rating: int + discount: float + + +@dataclass +class PartnerUpdateDto: + id: int + partner_type_id: int + partner_name: str + first_name: str + last_name: str + middle_name: str + email: str + phone: str + address: str + inn: str + rating: int diff --git a/robbery/master_pol-module_1_2/app/main.py b/robbery/master_pol-module_1_2/app/main.py new file mode 100644 index 0000000..2f48f74 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/main.py @@ -0,0 +1,11 @@ +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QIcon +from pages.auth_page import AuthPage + +app = QApplication([]) + +app.setWindowIcon(QIcon("app/res/imgs/master_pol.ico")) +start_page = AuthPage() +start_page.show() + +app.exec() diff --git a/robbery/master_pol-module_1_2/app/pages/auth_page.py b/robbery/master_pol-module_1_2/app/pages/auth_page.py new file mode 100644 index 0000000..2881fa0 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/pages/auth_page.py @@ -0,0 +1,94 @@ +from PyQt6.QtWidgets import ( + QWidget, + QLabel, + QFormLayout, + QPushButton, + QMessageBox, + QLineEdit, + QVBoxLayout, +) +from PyQt6.QtCore import Qt +from res.colors import ACCENT_COLOR, SECONDARY_COLOR, ACCENT_COLOR_HOVER +from res.fonts import MAIN_FONT + + +class AuthPage(QWidget): + def __init__(self): + super().__init__() + self.setup_window() + self.init_ui() + self.set_styles() + + def setup_window(self): + self.setWindowTitle("Авторизация") + self.setFixedSize(400, 250) + + def init_ui(self): + self.main_layout = QVBoxLayout() + self.form_layout: QFormLayout = QFormLayout() + + self.title = QLabel("Авторизация") + self.title.setObjectName("title") + + self.username_label = QLabel("Логин:") + self.password_label = QLabel("Пароль:") + + self.username_input = QLineEdit() + self.password_input = QLineEdit() + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) + + self.login_button = QPushButton("Войти") + + self.form_layout.addRow(self.username_label, self.username_input) + self.form_layout.addRow(self.password_label, self.password_input) + self.form_layout.addRow(self.login_button) + + self.setLayout(self.main_layout) + self.main_layout.addWidget(self.title, alignment=Qt.AlignmentFlag.AlignHCenter) + + self.main_layout.addStretch() + self.main_layout.addLayout(self.form_layout) + self.main_layout.addStretch() + + self.login_button.clicked.connect(self.handle_login) + + def handle_login(self): + username = self.username_input.text() + password = self.password_input.text() + + if not username or not password: + QMessageBox.warning(self, "Ошибка", "Пожалуйста, заполните все поля.") + return + + from pages.partners_page import PartnersPage + + self.partners_page = PartnersPage() + self.partners_page.show() + self.close() + + def set_styles(self): + self.setStyleSheet( + """QLabel { font-size: 16px; font-family: %(MAIN_FONT)s} + #title { + font-size: 24px; + font-weight: bold; + color: %(ACCENT_COLOR)s; + } + QPushButton { + background-color: %(ACCENT_COLOR)s; + border: 1px solid black; + color: %(SECONDARY_COLOR)s; + font-weight: bold; + padding: 5px; + } + QPushButton:hover { + background-color: %(ACCENT_COLOR_HOVER)s; + } + """ + % { + "ACCENT_COLOR": ACCENT_COLOR, + "SECONDARY_COLOR": SECONDARY_COLOR, + "MAIN_FONT": MAIN_FONT, + "ACCENT_COLOR_HOVER": ACCENT_COLOR_HOVER, + } + ) diff --git a/robbery/master_pol-module_1_2/app/pages/partners_page.py b/robbery/master_pol-module_1_2/app/pages/partners_page.py new file mode 100644 index 0000000..9b2e804 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/pages/partners_page.py @@ -0,0 +1,130 @@ +from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QScrollArea, QVBoxLayout +from PyQt6.QtCore import Qt +from components.partner_card import PartnerCard, PartnersInfo +from res.colors import ACCENT_COLOR + + +class PartnersPage(QWidget): + def __init__(self): + super().__init__() + self.setup_window() + self.init_ui() + self.load_partners() + + def setup_window(self): + self.setWindowTitle("Партнеры") + self.resize(800, 600) + + def init_ui(self): + main_layout = QVBoxLayout() + self.setLayout(main_layout) + + # Заголовок + title = QLabel("Партнеры") + title.setObjectName("title") + title.setStyleSheet( + f""" + #title {{ + font-size: 24px; + font-weight: bold; + color: {ACCENT_COLOR}; + margin-bottom: 20px; + }} + """ + ) + main_layout.addWidget(title, alignment=Qt.AlignmentFlag.AlignHCenter) + + # Создаем область прокрутки + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_content = QWidget() + self.partners_layout = QVBoxLayout(scroll_content) + scroll_area.setWidget(scroll_content) + main_layout.addWidget(scroll_area) + + def handle_partner_double_click(self, partner_info: PartnersInfo): + from components.edit_partner_dialog import EditPartnerDialog + + dialog = EditPartnerDialog(partner_info, self) + dialog.exec() + + def load_partners(self): + # Тестовые данные партнеров + test_partners = [ + { + "id": 1, + "type_name": "Золотой партнер", + "partner_name": "ООО 'ТехноПрофи'", + "first_name_director": "Иван", + "last_name_director": "Петров", + "middle_name_director": "Сергеевич", + "phone_partner": "+7 (495) 123-45-67", + "rating": 4.8, + "discount": 15.0 + }, + { + "id": 2, + "type_name": "Серебряный партнер", + "partner_name": "ИП Сидоров А.В.", + "first_name_director": "Алексей", + "last_name_director": "Сидоров", + "middle_name_director": "Викторович", + "phone_partner": "+7 (495) 234-56-78", + "rating": 4.2, + "discount": 10.0 + }, + { + "id": 3, + "type_name": "Бронзовый партнер", + "partner_name": "ООО 'СтройМастер'", + "first_name_director": "Мария", + "last_name_director": "Иванова", + "middle_name_director": "Олеговна", + "phone_partner": "+7 (495) 345-67-89", + "rating": 3.9, + "discount": 7.5 + }, + { + "id": 4, + "type_name": "Золотой партнер", + "partner_name": "АО 'ПромИнвест'", + "first_name_director": "Сергей", + "last_name_director": "Козлов", + "middle_name_director": "Анатольевич", + "phone_partner": "+7 (495) 456-78-90", + "rating": 4.9, + "discount": 18.0 + }, + { + "id": 5, + "type_name": "Стандартный партнер", + "partner_name": "ООО 'ТоргСервис'", + "first_name_director": "Ольга", + "last_name_director": "Смирнова", + "middle_name_director": "Дмитриевна", + "phone_partner": "+7 (495) 567-89-01", + "rating": 3.5, + "discount": 5.0 + } + ] + + # Создаем карточки партнеров на основе тестовых данных + for partner in test_partners: + partner_info = PartnersInfo( + id=partner["id"], + type_name=partner["type_name"], + partner_name=partner["partner_name"], + first_name_director=partner["first_name_director"], + last_name_director=partner["last_name_director"], + middle_name_director=partner["middle_name_director"], + phone_partner=partner["phone_partner"], + rating=partner["rating"], + discount=partner["discount"], + ) + + # Создаем и добавляем карточку партнера + partner_card = PartnerCard(partner_info) + partner_card.doubleClicked.connect(self.handle_partner_double_click) + self.partners_layout.addWidget(partner_card) + + self.partners_layout.addStretch() \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/app/res/colors.py b/robbery/master_pol-module_1_2/app/res/colors.py new file mode 100644 index 0000000..b4165d4 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/res/colors.py @@ -0,0 +1,4 @@ +MAIN_COLOR = "#FFFFFF" +SECONDARY_COLOR = "#F4E8D3" +ACCENT_COLOR = "#67BA80" +ACCENT_COLOR_HOVER = "#529265" diff --git a/robbery/master_pol-module_1_2/app/res/fonts.py b/robbery/master_pol-module_1_2/app/res/fonts.py new file mode 100644 index 0000000..207a164 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/res/fonts.py @@ -0,0 +1 @@ +MAIN_FONT = "Segoe UI" \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico b/robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico new file mode 100644 index 0000000000000000000000000000000000000000..9744b0addfa0c52195b555f4d633f69949bac01d GIT binary patch literal 13342 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 Xim}#+DN*nnJ`jsD7tE?n-0%D!1i<(G literal 0 HcmV?d00001 diff --git a/robbery/master_pol-module_1_2/app/res/imgs/master_pol.png b/robbery/master_pol-module_1_2/app/res/imgs/master_pol.png new file mode 100644 index 0000000000000000000000000000000000000000..c192a72b9c4738a50e53cc4118284b65570ce9ca GIT binary patch literal 161727 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 = zUh?lZ(1to9EOZ(P-|MwYU&(uf?X(dPwnV?j^#kcNeJW}(rUpLdF4~np#xK6NwK1AF zB0tVgo(nsk)oiT3xrWTu!)_!*QohnFDD^Vp;3tQmWm(-J!Dg~e1vauP8o{*dNTfRc zv!@0G6E?IiEk1-2%BJi^=Q27wxh{cjYKeZaHdRq(26i>nmGRQOaw6z&`YZOB{;{e4 zk$!DcsSuHbkp_X*R==uSDSH`KteU{x)>U%%xmhcQm&qv{B+lD1}Q#U-=RW%Qm_Jp+Kl~iz6<}l=t*EObMU|$cV|@ODU_R>A zuqA)kbK<26%Oeh7dE0?I8430SH7iJHK`$R?x+)_4!J6E*r+wcu>j4uI zC#R8>8j(4e))6u`c>8*zo$Kc)e^QRT_*Li<>)g8$wAbwEswva6_Yq!4xGgkNePO5j zgk;c)J=d$Xd!sg=m{|pqpTxBL1;}wKXBpCvvNTRiEJpgyf43&)ZaSBl$9(@sI(6W= zQNe&li4N&A5-d|wO}Ob$Dz$cZsOe*6xPP)mgQ`DUuL9BcciLA9Ma=GoJ>AZ4qOP`OM@Dh0c!H| z7DEhXoK9ZOKISWESg21fx`_!~hdcoz9OAmprl*%iH}oc9X!RE`k5bKl>Evq)fJPTf zHP;_-tG!>*xNZnq5?JD{apdF|5QWuEQZW|R?Yw_jkzMs)ZFAx@NaXnP^725CXo;alA~*6PpNqt~R1Hk*Ec!D#V_jQ&2( zOTMURH}(r|WyQhnz?%`Ozs|dFjJouKd&qI_%W^0->6<#giytmqIp}TvjPZYtKMRhD z-vWHpDp^7(hxi_|Cj z55~(#%e|Up%#U|D4WvFktM6gE-n*>1fGnNhL$8Ugk@M^CGfM3ba&`fA9(anJ{f0*9BRM^9p*Q}3p>Qctk}PWM7q254Q{1!f*^KCR z$`#*j?W$uo_-o;;txvlpX#{m~3(qqW{&<%+8CI6QJmq0$A$gXW_$~-;)!K zG`dv-ck}jAEm4MTtj4RQTHVsnMPD5}S;a}(8dXsoRh1XtgHRu9G%BiVEe?bgHVa~P zprxaK1XCkN@GLWW_CvBUht}FD)CK|ZT?UG!d`u;hW~RBhbwc-Uv1Uih$S%^RlR=3E zwKZy<&($iw9wRRwnv2Sd{+7n2c5G3x zcTDiI-8V#x-<=-x#;poodvdg%NhakVX8Qi-Fy-zJR11E-rY30!`?2GVgD$IU@~+cK zDxP^ZOFb$vetLOaS8>a)p~C*#|2gU*F_*1tI!TbUoG?AVai&kD@;?f-Y~E1x;5HdS zi+81@{Vs!LMy4-nZ?NNaP6TD@y&^Yz`6OX-F>e3vwy@{)Q|j9O`hDLb*Nd=X^w?Q-1rff0EvAxaw!6 ze=M-@@9PhX&jUG&M{agB)9&!M;dkF!w2WqcSurCx-&hIUj0xKod--|KK3#n=GGL!z zK>n?W`;PIwD!)~<5DeB-pTtDQY!xJhTt&=q)i3)2`?AKjoxx6W4Vm!#fOz3#n%-0WhAum{;hS0-kd}j`TDXcWh5W5`r8m>pFxnY6) zCihRQx+`8Queq;_2IEdplsCO_9es`q+H^I)Ft*j_ZXJB|jK(eL2d*QO(1qQ7wNPdWV*i*btewgZ#*_Ctt<5yBwYk0p&nAlSY_p|VdD zv3~$RiYu!M4YM)B7P>h)BcSZTkJI_PDC(x6QRTgw!%QjHi8r72 zy<|j%zm&r?G%O1LWh=)ii?=Q#&;{&iddd!HQ$_Yc%u`EDEIgr6aG5MA-6F!8Al8t>NbHtGQuoRj)h8*ccvXv0m55cOAE8!J#ev zZ7nSgNk1sVD_L)JTjVZB|DO25R!+sHe%#L8``Rsio=v8sWjqagMmNK%$Vg7_n`ngb z@U=0bSyUj{?JYJ3=Iru)aGxD@pNfc<2RKC_k6r~*_-E|zxo~l)pT1RU4%gl)8qz@P zTZ-~`?G`QL8VLSbHf!^DP?m@{j!*3}yg%LNJR@=p&%WRdQ5#+LVr!vXYwaw7um2-s)et5%*IF zm9G-HB^HtRt0Tw!TG#1kP#vk$8ab5cw*?}MoFh39H+>w!;5gOw6qz}{_Wc(y*X*15)zfH}pI`1N-fw-irhv-j2dFCkpUswiiAC7&U7@z7Hgr#zr;*jJXr7F>5Ys;R;FM(KZ^R?OLwP_YL7_jWgYrE-_M z#pjGo&nT_S-EN7Q{%ah$dzkW3sfEX4%x3zj-u&EZAjmpOi#6urY*6mPb-7&ihNqnw%Th_Z5(M5Ln-T)u75`}}LSmGUy9JsQy)_jli zVFCsy=;1wADpf+&W?2>TX^G|Vl-5PhM2$sX?&kgS7hh%_B8fr_htZ{X7NC%~`5Q`4Bw=mkT&lkQ+T+#d!-J`` z2()2Di^2WxiqO%Uw-n~lW%_sXRv% zO-Xr4bZ~++^Yy1tZ9Ep1jB+st=r70r)6_Y+M<6G^ca|C=r~6f+bN-!TW{Updj+gT< zJ60-vBk#^Y0K~o}(Auu!M$bebA!LT@V@f|=$MuVfNyPn(u68q^m<|6x@!|uUU84?HNyxRTmTv&Eh zG3UQ0B#kB&%$;L9q}1OGr8481vfdq1Qs+q4)esWaS8#i<@{XX^$EWk%>-NDFv^YLA zJik3F&Vo?UgblBff%u}bF|ifqaMsqp_e~I;9q#sjnqw3BrJ#ZAZc-YWt-Mhr&z#Kc zB69M(v1i}@->N5P*0>MnalEEWqAXdgCsMo@%UvZ$QQy(O&8#T8c``XMw>no#Fv_jv8el`p9wQy5C@lhj6FWqqH z?gT~kzJ|-&BzG@=*mKHrUzF{0F<~Z+d6h(Oixz#PmYB74 za0yfZl|GiN+Wv=IJ(kj@dPwfCs^5re`$(Id&KEh)X10zp=LJ>JFi@X=XuQ1aE5S+G z-;m?10Urx`ooS~Zelp8s57>NX{DSXCVtX0a8{rEXNt=U{f$&D_)%8J8ipFSc4hpr zG?XOAcYQ}}_D{biyL&{9iBeR!yQFvsG zPdjm)@8wx~X-8jrw7ZTD$`Spd21S1yE`>ckHc>4!^(6S6gzU3yB-JHzeLgXyd5?(XLqA1xgckj4wbo(|l6Vn)i32D8!b zEd`4qAKFUcFo8s*rW;nI`|`A(fWj1Vdk|x9UeDCWg)`?Z_Tu|PnQK+K5z>x_@*KG8 z{gEg3&A)C~qm?snrhlybTYQz#Q`{br`~q~lb|Ta>r2|Wo!lqRB*qqC{WUTq0>N^GR zi;w&ypP`LL#~?pC_JZ2xaaWt=5eBOK)UVncT04BKv(9)7+_4Kd$cSvqwLM|HUj(3Uo@rVmKEO60?BNDj09lO<**g6A>c zxImk`jWoN5ty-MA*buyzQhd#yzK&o7*Rs6*DH8Q=Dxqo+G0=fOlS!0neTd@=rc zp*dwGyo8G>BsBdF;lMp~uA%jdL2k{@YQ3&-KVs#=XM_uby3~4{SwLs7RM(lb6|!cz z`OkEhFo$08mU7(nul|oO^0q?P+F?R+gMH|rQ?eci&M%)G$ipT18JgFo{*7p1BFnQN zhijp~Bm1S<)1GQ4TI}gzT9KGZB5`@5|5pm?jN0Qz;QPtBGSc5X@)9!H8<;6h28P?) zs<|q)2`?DWYIp|VFkEH^t%~dZ zYmBWbJmoKn-Os~|6IlJpDJ$eK1udY_-Au%f$Ma*xN%Tpv+kV#`l|v`JljLcw_7j=g z-06~b#81*gt?JzmT+qJ!5`XXXsAQ?$;go17%hm84({F@P@fT-g*ALo2_GH%9iu|@` zuntdD+qG&wTq2tL^6T$kih?sV<1>$qjLW^J9`gR5YvMPyW>8;C(;2Qdg=Lw&t(#&q zUM2EJf=8){iK}mHsaOv-+k!mUomnuw>(e!tfC2uklcg`9dY^a*W$};!m3aFleYlo94cBP2pvtiJs#vj4z@*Q7JP1rcqFG#C-M|+uI zjJ-uyOYObkkIag3&t4ZM)$zGUZ)Wt>G)VmadId^dbGFySb^+c?W`?O5t?sn(qV!$U zKz8&C2z_$?lS>B8eI z#R@zG$nwwlyYs|kP)~Z$DrD#vhiY>~OGacxaT^lsGH8f>T(8qj>grXDUAIkbEIXoz zR?fcR=^_vC*wg~2L?cHoM+5H%`9sAGA=L@`O8h5AAAsh0>Zwbj)onvQQy!ZX&x?nj z7RaVI^yIZhFq%CF=p}Zw6I6*@0)ryFg+}&ZkXHyx5~yU$7epn?3*y2`g+*cl=AR4^`UEoL!_+y{PFqGE)q*5X)Ryt ziLN%akf=>4nIAGygYnBloM+#PxgozUKF;Io964VxuuEIbukRI;ciXJ?cXIW=N0Lc% zC2C~S(JZA@Mbphq_-|_vA`#85=}EMDe_cNB&9CI2S){hi`? zL-hAE`gAc?>B!naAV8$8WuqT_{l@JKOM7@2^((DW;khkqg?_UsDq1 zVns_61xy(vOGOJ@?|Jfeax1$;cWQh}2JW43(lIu|{po&&vbB-o=h0`Zzdkff)Oevj zbuLHNNbmfNEbm^{c9r!WF98v1j{m!T zqB}pg4EpUAZIpsmaY$cN!ow>h_F-M@!ETF}eo-Pn*PHm1{|L#(#=gm(A?b#rl-Ykg!PEZc{R5_l^cE{3mwLL8?Yp2dy z{OJ>@TVQj<1>8KgAgQ~|K06-gGr!393HtfpRDU(RG9tG{IU}hh(*0J@Oj)s0-oK~q zdsg7aW6PoW_2uUl^o}D3y^)_5Hec>}NUT}!|E0p9{*Ts-lAHXDDT|m#uUn)M>a1+t zPP9_r>f^{X$p~aLIl|1Q*;yVCJR_nRAl;QI?M3DRr}`l5b!n2b9X=C`H6%=$jp-Lx zbbJIO!3I6_uz_ZNO>OB0U*!6J_6G>GV*hApmF7)fKsC|VX~<<5cM}W$3?*nzV6tLF zizcl{lm?AuSF8%B3mOrSCuBv;Lf{>x*dsO;i5d?iRv?l*!HH#jMeJD~&?ecuU$|>8 ziJU6?Uim(>H#+Bj7=!P)6ZIbcl(t8=+EQ;ke;+ra4~+0;8R3K_3R-?VLRgjd_(ANo zZ7hRhkMJ+vD3vijE^?0*9Z)2K3)=Ad?r0po1p|V+rsa#rCyIDtM;#|GS4j&RbF*pN z9iSm?Wu~`^JW7xcYCH28NCUI$v@g`e!}1Obi{iceRwa9K6b&6R8cJBQt*3j^Ep-P* z4j&+@j2CWxk<%hcR1Aghrp2G2MF?HdNarydB=bg1$h<$iqG5m!FoFRdHc4Cyxe`xU z778VK)X~DM$R0t=ml=hTr15A9N$EM4y=Z83fZY6p5wN#LtjUoF%D|g7wP`j*?S6Un zs5cP}|6Kh>pzEnuwgh;I7;YB7RgXSijp|8`gYIBSl)LggJQ0`V1F3V`LLK+05-yNx zZG_&hFW{2or{uAP_n$Tj1`JlqN2fh8{U9>qnUuTY{r@rb-SJen|NoKfP&UV&2ibcQ zvRATaR`y<56|%`DtBj7cdk@i3nNB8)KM~4U&nRa=(K<0bLFIZ$E5lMSO;0r}LI^t&% z1CHdo<;@KI*feOl$M@IRQt^*JeH2tWBm!%C2bZtP%(G>C{v-cie$9WC)v;!-xddHZPZ39ZF)P+tlFdSKvau z3yz?+te88xNfPEzE{PjH_)h$e^Pw;yVdJhw zK@t7H!dWc$^T@mGP!^VDwT)Y_v_wAoQpn2Tq4Z-Kyg^MPW$^4>1d(Y3Wlbvjv=4il zX?p)Gg}Zx)-h^t(edkzFjt|h^_Pci(FeYP0<4c`}Fj?9rXN5igj*Y0Pkyn%u?K_rm zyWk<8ojt2Npi4BPQV@d3uGkz~hhIbSLBJOz^Xy<_*w1iq>|md~q3h^M=w}_jxt)Ix zX~QNbOI59AY%*`r;`Y(J)BdqsP@k-YRG%GDfnQuk=5)~P#jtp(+3l>wta-CFwDg&HzS(lAcP{soP+$a zp|Leou_Rz9WJc)!d?O07=doe9$fw(Gd_p=}(4nkS-7)S{@QHL|ZU928{Oab~0v8w? zC_eK;?;nj6X{bVJKwqm#OThF98i@N$vSEfVF;SQuU0p$+bDmnKfI(XsznCY zrNfyxHXTntJ#b-nttusm%ni7AMv(Aj=e_zN@ z^zgtmGI{@fEk`Y%_YI`SBl&GgZ>4FLB{%o+@3W47)Krw1-hq`-}Ko8k~Q-k>Lc7%v~@}r1(fqt60ym!@iL+=6d6f%UgrR zwkU(x0eqv#On=Y8=v|VEzzX-%DgKlcYr}L!5~UvaS2Wdw_mV$;A>xSVKxAe#hkg zB#>aPZ?jTU&BMNxnmaKML&aI{$Z{&S>RUCLmwbF7FUWo z1A^DGc5|0|htT%s84t-T%IW-$UN|5VwT{ zJ4?8@Rv+**PK$-6acG4@6z=6aD}l7&M*xBD0A~j#5j}<~ZnDA(XY4zH75X1!z6t*+ z@9#1L8F|4}G1)BB|7!@tQ@JD_!C7oigO3RkcgNJ(`^S2M$M3HrleHezH+oqoRXryi z)U{U%?4N94#)aYl5;qL9z5S5E4Qsx!@j7%Vq$8|(8fZ*OU}WoyLDpHH3RA2DBr(J{`>rwQ6-1Ym5VI`bvUb3OSD{6%o#AG# z=Ju!6>Zb?vu41rSxyro;l zJ|`plDiwVmHl{i@_zVef@*A0IKQAZ>iyV(>q?48Fd`_);kgMyzX7f82*X1>P{CEs_ z4J0UN0HtbL)n?4EBfU;;yOwilLjy}Om<0+rh36%%ou{kbls|-ow-_!X;#=#-KensA zy;`3^F6nKvFF^HaM|lH7;C=jrGT#y-z8#$RR_`&H!nQ`{qY>L`QvXuMancVvP7?ICCoysuNmmNBO57h&H)Pk2iUljs{7HBDqm_%FOO4= zdTFbUZRZ>Wxx@lKyh(7bUD*;^en8B5nwd%QkW+<(Eh5my(D*$KH4C%H=h0ZX8eS@| zMrk9%c@v4 z7hWj_+px87n5z|(`akQB`)b5ekTxR!!4*Xcu{HaHN1L`DUy03u5>c%C>GCi26}r?n zdN4}BB15-iX&aDWT%1FB)1SdztSD7ji$<@biy9F|_(CfWGyOWvFvYRvkg|udfG%G$ zEi#7QV*~C3@_fo0Qk*i<0U!Dp0BeFvYeSP{q2I28&S@teTl*)ZYz#+62;Ul)!u3e~ zz^{HOr2p}#rADhlmQ~h-VCEU^;X8h{Msr1M?6As8%#J=m{=m@)D-23%%h^wSFg&d% zw4N`*Cqu$EKE8692XH3YAx7z6t6Tg>{|JhM>eicnQrQHBwC8b0~YVI_8h!asrnDe3O-@+&eckE#o{%Qf2CfCGPX zQd-wpwX4&z_*EyY!liAn>BqX=*{OqTji|^&AT0TCDXflsw~xpv7ShGRlv6%EOI_ptU8jD>vu!xWg(A z-~NXzM6Y$;zxJ2M#z4>76)GdaYZ^Ff>w{Bh_kMY;Q6Bk3a=NFQ%Xyxoh0-$*UB=l4 z%92$wpmZ9w2FbjYr0P#5l(j3KDoH-=;cjN%9moFmF0ZrFZU4^ABCi1*cV|X)l#}$Y zkreC88P;akedNW7mZ!^e=Ts*d@2A`s=p4lg96gr2miDJFmKW3=kO7Laa}W~)qL`H;}-dv1~c!kKG{>8y_iDo-geK*Tscu^(iIf;?;*^hDGeZH{9zOj&=$f%3o^@(|FFo zQI128Q0CLU=o0MVh#wK8sIEZ)Dq}<-T zbkHkUo7n~*qtob7*Qt5(3%%sztt#dtC-iWRRS)-W)9J4s&Oyr$v6EB<*!5jgZC)qo zkQAmN_SU&h|1j?hi)j3#%Au|%lt~28H=#y19%Nf~?owMZBu%BO7UJhBKmI&--u-p> zb?wQ`%mCZZQ5+W3o~o+tSO?^d50U3bLT>IKTJ9e}Lvkj4)QpVd(Iol)K}btJa2X1n zlDe8%*Z>q%{Q&WWUX^{|fTwVtFrGdo+_!n~m^}L`GS}@}EoKi15>ykFFw)kaWJ<$; z-D194X|%#U^7M;GL0W+xR0n#legawAaV5`i%i}}K+uP%%J~uQ5;<2$41wR|Rk^-L% z!wf8bV+IJ*VAh;=9ZLvo>_el?-4mD`^Dj@~Q|@MGdDl06RJmnUyO>~I9rxbM*Zz7Hi}d=@A5|9;X5yKXykAh)SuMzzqD7MK77{|EQZi_){9C2d zy#4Vr1xphG|FMOw1d*OI1)6O`W~+2wIl0Hn3|%ZLr=bvdDP`UhYiF%>8+?jmNoP-i zPpbi%99Wg!7c|KQ#tc0B^hvEHX~|rD98O}Cx0N_n9TseEX1Y$zf~I?XX)xlLl_PQr z_s{y+V@i!^j|87qEc5;EL~3ZtysLWEY#`l_xDqrj{hgdmqIIeD=fk#Q%`_o!$ZzaW z69#Y?H>%$(${yk&xKTQ|YehO^EhWkAUp2h!@vJ5Crg!P0-*_dW!kUe(CF?GWVsrPf?3oB!S1s`ccV!X{hm#( z=M>nwN6U&vn#+beTZH}_s#Zo@W~RUd;lH^yq^X5y5LSVtbmOSV6@PbsYwZwkYx0H} zWd%DSJU{5iTdF-pt(>@q!J|&0UPoKW>EP{TGn=m;^u)z9lf&ubp4Bx4xm=sk_MC%- zbGyfBi4#m5CHkC&wIjdFdo<=psO6rUJ7P}l z)71*Q(aV05zT*=xHW>5(CD)uQ0mzT;VjVS$F6BM8z){ttlpc@z#H|c ze1JFKPsQZUf-n4lGmQlb6?4d|6!?+p{oi!e!04lr4E})=jda$YEw87N0nfGetf>A$ zH9c^PpU6<)gU2(EOsMw{ADy_m_8?_&T`Q{vAJyqBcGDgw5dP=$7vJ5QW!D zz^}e;TGPI6J|DZuyGyQH+_1$`KoPGK=`({rsncJ^c;3@BUBJEtt z*73aJ>jR;eJBI9OWS%%GtXS;4N_?C-$)Z9_+3o{FEVffkk-u?m&}(h-hd1w?a!!Bz z{<##*l!#3D4jozR!zl#{#WyeZ8@cAK<91oW;R8tYpl_BepAe#@CE@zXqS1Ipk;iwF zRzLFvv*N&PtJs}d`g8~mm>nzKf35`(*e*XbA=4qTAB$Icxsz1>kfh|&dW6ne+|x)( z+Q#x9HzCRot}XZhJ;S2)GKK4bd6!RhKk`7=z;aclzz@#Hc1;VOCl3lZHp`3F=k`|R z(GJpIT$}pC@_`M`-z6hfaLw9tS5C)hT^|Am62tWL(#E2thzPI}wi+}B21WpWluM2H z^;jdKz2W*(oxG|XiDFJ(PBpBm3+Z%8UAl?HTJ69QO}nRM+&q*cstk2^&VFBMO;MrG zoQzRKq{k_(CLpjdnbHz)Rv=1M^CAJ;-=h+K*X$9qy>mo?ElJmh_+laR*^*y@4dugD zpEa1ohI=Rux>BTAO>p6L0+_M`N)5Ql_o3CIn$z#s1Hbf9&Hv@s2`{nKX`ui!JN+5o zhd+5U%?`9Pjv*~|?Og|H$t8i_7yp{&wZG+xzwGZ={S%$Y4-`BniEb5j5_m*#$?~VMH5o1?O#G; z2~&Wx%>2#xjRD!#$5gdv(CESj?Ub@rFifnTbI-wk`v5H!b|>Ow z(|tIcsP9WW@KUst^S1E>aAzxsQQVfsqvj$UD231$X5%qbD5!HX#$&1DdGx$uubFiD z#?lv$!#h!RD1|lcabU_eUw^2aVzIvTP4yL#|G zMZaD-hBy_GUeATxMM~SttlZ@8#7%N2c%*9Fta0jC-W@)UA_gy$o{tlv?;@Kaop~{a zQc{Qy5Vg8fUsx3tP>JgOqN%Y8D7sY}Y{IEQ#d8aFCB)o5`XMHT@{L6(p;g zER-xcZL9LcXdmK{69%Ez!_n$w+H9uHOW`LhQm1~XNQUeRzGoi|RmF8p7cRo~i13zt0|Tp7{N8!aRStOiJ4S>Xi)4*VF3~`K1XS zzj23^?dMru*bsi1GP@?aoGMKkf9}&eR((rf?^&1yvc+EvIvwq|b?#EDsV7Y9z*rMv zgs;@_q6CFU856AMcK^N@=y9n*rH>qs2~D!;4)UnNTYh{m(Kt{&f2L5&hG+A{Lg zYC_RTrC7JX`~f(#1L|9iz~c^4IFy7 zw0^;kRj_`1&V0lc2;3N^Y(m`-Dq^zp8}W8Ot}+kMW6cB9VqjzIeUTXUkTn6L+=Xd! z)|iARUU`klWxl`^s&izD@C6pwuKzK}^hm}{V{JqoYI8}ycG>X?*z3_=di2{h%X~de z^)8Yi;cbx5F@u}fIiUQ+8;3iu@FzWSrmLz=PlmpIvSbYhV@63*;O-t2>9TgajfTdw zVRgCk4B2x1f|?UgPmLBQE(W=J+W0#->H0DD%Pn!`Q3fh}|F)){3<7R9l;x$rZ-bZ( zPf#JTEsDFvf4e_wa_OE(HwDYGqEoz!QR%eYSnrG3E+Y9i-F)aTgOe-gcfRp_qo{m< z^@VoVV=&9cvpNCf?AdLzKR9YmhjA1i{1uh)4D=9q?}Y zDpLQcTkLtXObS%FAJd-7O&RXcdL*attxPoZFI2_uH=VV`u8CrQjq2C!ZX;SA6N_8R zY&xmZwDt|m?JN-fH^sJ6@%)ew=sGuxocWekzQDu*9FISGfpm;m=xLm$t88qAPm3(C z1!%qw1(MdCJ$DdJN?EH*>TUbT+UF--vMTa}5Z(7Q;f!Zz8DXVa8!9t#wcK3O`MaA! z9vf3GbL=M8EHa6fCaUWVxf94U>}$OBb%!p0#l3@cyn1w2N?pM(BQg9SFnRcd!OY`p z-fi>?{!NF|7d`Ldi1;<$5i!O#agsfJ{7p0gz+#D-<11~41h>6b_F5d^-pGz;+B2|o zuVux4g@{3Vu`Fq8-dVY%MWqm`W*s)VFtGK8Qr2$UCD3q5xX|{fRLz~rxVV6k#q`MN z!+?JMS3WuJ@u){%82}D|!ww*UwSK@Y&-`a1ideO6uq?~SRbWJ-EFOXXf4-ONp#}BK zRmv=#m8%`N7*v#$tncUq)HuZ1ZtK8;+hr{6kx>@&&m)D#MqG}NMgbWhE&}vAkbha| zsqDjip$oMYVr9T)dRsd5a+%+0EQA{U7?>6E{{Kf}8IlmiCcZ4GOcT>={8^Wzh)s6k)SA zk&Xi~l`Oshlt_%%)N9_XDA9HX0z^HMl#su6gjRLoLLF6GT@fK*j#BBroeIE zX9Qq^_|s4C4SGgtZSrjzcB5oj-HW>G(tf^ zj2Dej|MITuARmxgcD~5<8m*g-ZN5-}5Wi;dDe3lJukl$}ua&j#NdHx{B)*at8voP( zE(7TpJ-aimG}rSK=0$;0E{3muH2OJwKbvv1o^aAxcgUse=-h#OxYJ0uu=nCT6w~my zWTkX@$BZ^#4Sdb!-UME0w}R+sV-F)Wfx2_1yg}22*0A;HPz!J_|LaWqWlIFv?wI8bqHw}Lf^>GQx<=fZ zxPE$Lo_TMKbGR}?b-$i^C{bl=V29fmsr5zoe=%2RU{-%A4p%XDA#eOeq3C2Kkpp@- z(JRpyQ7I`~6zSyJp5=JMvsO%p7c$k{GrEYR068wzQk~u=B6N5aG$TmAo5E@Lp{A!L zh~IUmOwmr1aO+}2;8bY@^4KwU{A?<(ZgHgDpSCae7|lWqUr!=ThwpeR_RXzI*MAXb z6fk>iL}Hd?A4s8CQrF1?EHcvNYn9866Uv%iR_K!A(w5a_9uT}T5|!k*6Zh4ARz{(Q z-@!mGF(MO!Njzx6KT5idPmtyK(Hl`d`Qw_kxP_6g;+1jP5MrG|;gV4RP6_5mpodlO9iBGuMd5^Q01O;b2__T78W%m(2Bpium$XNkLXDTSv`7RIwZg((8*RHBs7P1xVg0DQ{8MRU$!?^ z%eLxkmTvG18BmkyL`ZWoyiw)HMo*w}xw-6RT8j1s8Xi{6s2E{`pHoEDbCy_&Afsa* z+<9_`q#_qQMCN4OgQb~JPNA#ja30&pk!}Pee&&bYejr7 z@hNW=h^A{L+l2B@r4g+(bE|zCdCrOz<#mLSx&OQ&;bG}IW7O zQ2*udga5-Hw~O{-5g?B?m+h=vF-5%OxLcqM7{^$y!&F$1w)xm-2&|h}u2eLOC z!LH`{_KwvFTXRs-^iC|KkzGA~g7t-5$BkTAMR|MIcuofxg|>pCP9GOZ zxLp$NPOcKS=Zm};|L;rI7u{dSsphLD_PG=;Xz-*bZW*g^O!8aT;STCV)MxJ~+!;U< zXIwd7BEa*&fwI7CL7;)NAx7eS+uk12Juh~DPBa0GSt;3&m98b}$GZX#HK2D?OTR#M zel4lYT2f6{f%(nW$hKnQgQ5%wyMRkN<wQ%p>l9P;CcOD$KY?&oOM1o^>9itlAdi2 zS<*NRc%V8@oOxLFD+x;5A@YE84VosFFy5khsf~7*D;yBx@r2&A${q%sqHzu$>kXKH zx|2{KKU_KPlH`~oH7eIgU=`YNJh{4#^n8Z&Wj9iByjoZQVHevXUFNLWCHH;Y8@RLr z&<*0nIJ?OaxI1j9l$sRfVS!)w-Jc-Eo~{~?W;>-mXgCzXkG{@ctxey|5ZLN!MLfhn zymCH9Tb~nMySmXxluvm?I&m<-elQD^V+!D661P_W)%iFJiMO2-$;vfsSsy{A>YvRa&t*WUdpH+_y3cumM-UK#4PCj}YA6Q)7iaSf zOV6XyyC#J4ynqi`ltB1Rw-1XH<}N%#Ry0-|t&HG4CbqtXAtzdJO@b07s2)exHy4?6uWV!a558W;P6M|2+0Sz(r_f5o6fvZw zq%EObOhphD!??GiclNfeh-`9zSamWWQ{>|PBV(J2^~pgGz)kwkfpTDFYx{k*PPM)<1-w(!SEy|4;Q^;rU`NBR>Vn~5Sa{9x1VBxct0{oTNzsMPw z(`^~*I$>O!J`t_HvAzp~iUbEX?j6q4J*SIYo_d9K;-|gb1yIi^$dsy;(e|nF8zJAO z2jcjiNw+0&sMUo?@^*^yONG*H`JC^QZ{Ov$i4xPiSbRQA48z<-*a-bgmbmxCAen*U zyfNGUb=~lR0FD})IthtehE2UG^l|Iw@t6>ql@)*zU0Z(sOH8X2cusi(e62oSNvi!?@xKZ_enTLy?o7i3$Gwz0 z-%OHA1Z}T8Fb4BUZ76U~shh9a+tX=7GqhFXhi+bHAoq*kCzab@* ziHwP{tfp=?1mO$?oT%k=GYN=K`xwGN+vlJf!5hHKt%F_c@k=kc_)nvhDOV!1*fc$G z)Tm4w?m#@>hViXv6A;l7X8v(Z4I&(bDJP4j43P#UB&SA`DDernMpSiJT^CvXQ&a-T zgO!S4FyUcHw_mIfb8(w?Reu??E$?$!`Sy;htdUq#>+*}6^YKQRpBLxNwd^5fK%#_5 zM&?^cm(Q;x72~9xBsDVK>3~kG=UxS57 z3bGvB=cWd0U+M`&+nznB0WNa?g(d6FZbfB4b{g@C4F&R?vIxH=j z*lNGyvov`9h31#n1mz>!{qXlt^Q7+R)aWhU+r~B7$fxRm&zGPoe?D0#` z=;_%Acb;PMb%;$HeQ`3Jzm(51JB0gg(xO~Obk}$G@u`a;c)?0ApEJ5LL#e?amzGuy zNqWG{5(k&##J?7a7?2UBz=KH3oa>Zp#IK2xfnk8n!-_m(m82isg__%oz}$J=g=sPS zyzlj|e=<^l!~^XIj+E0L{AIzq4U6r%{Kxb!==1tG(c+ z;G)%xUS_)tBu}+f^J}IYTI`gzxnp{{M#`dfaf?ZriC`DbCngc3Jkc*; z6bH_3kpVhZU-Wx8FP|~s6!ONxIXmz3P?D~6h2JkD#nX?Emij6%%iOfmDWc6c+R2oD zVH+f)kx}UkTmdRMBtMegWHHBlC(>4ZnH&>fWc4?Amd&@KKHqz4W(xo6<6Ns-xT&7x z{6ddAZ6L1bjkhrzf(ifsnOksMhtk9%iz_>ko@?+dTDphOEMVz0kjIi7;@n3O3xuQ3 zkm7X1$dUq+1=Fk&XUugd54OIOp=>`-Cd?8aOH*Gk6d3~Eg_?g^Ip+`ARAwYE~}+5i5Ym{ zVI;Uz74#bJLfoH^dEQDIKr{bMwUBCUXlyUOsfSJiSNVFrx&0jUB$GTEGJv031->Sv zl86{UoAc~=zVpW5lUM3zTZLp^itvbBLJ!v?TE%--Bn-txx<`kS8C6pL1)j;beVgZ8BxFql|?jmxR?gFquET)jjYUJ?x6 zOFOG8^D|7_U0Ar0aa!E!9bkNnGEXY0qLJ)7Kd6an5RKjjS?yh#!^uB>{xWl;@B_DZ z{;j|7?JXES^?Zu&Qe3{(^+7u+!*Ed75SSqNay26->s)Rg0#unw9>8^GGKDVVwI2!E zND4->HqE@d)NN8si*>{Jz$~W9@x?FE6RgMYbGAQcG~>IRFN-^?-Er}uoIp4hkk+?m zpD%u-Bdp)udBq!Zy$u0{1n{S`7ZNR^T+Is1m#W>&OV|sJmv)74EX5s z_V{M?8WO_vptt30t6#{Q^v~YZaYgKii-PR$#|=&_?1U`Q`$nc^1BP7plg|4{U}S}- zA3x|rAD5+8HuH^YCgA@kFW-E6$jnRn9N5CT)Z-FY4WYX-G1m8E8e0;6EUK=8rVCZj zbOCmCd)uEkcgKbkPey6THtjw>Q5A*#8OQkCx3lwYT~kGo2-7=3>%6s2rhj;M8IL6Y zl(w>3^ilD;{SL49WeJhvvAYHSpO%3toQvCetuyOtm!SR-OF)ENd3)yTBas z8yC;=akAr3KWMUp;kd&&-STCW-j4RO}7{5*= zA~bj-)n9ByHZN4NWle+cAlOuxAfnx^0!^6UzBW`|32c)n+PT!)QnU z@>pw=8CZm@X>_LSSIUR-s{JjWs;8t7>jkYsqA9nBd}u&g){6!N3vfMp%Q+QTJ*P0= z@c-e2LaCe30>cvioA|=ot9xX4>C(?n38jG*)5fkk!-o9oY3$-E9hBwz=Ed-?R2%G^ zH}`5M2*l+#{|08I^}DL|nrN8~I=dYkxaEkd1Ks=f%R{sMKv_cjW?Lu9ya1`VZSy*G ztS{G`q=AP%Eu2}W`xrIt1s(&Ni$43=#>J!atZcgXx+glS@gwSam+R`GJW7wd!7cHc zc1v-Go}5+oueZTQ>=R?L`eU*pNj4<=9v3tI;+}PC>O!M19L^cr(%h_6AuEOo+X@l} zXlM2K(G-A;*qVH7D6ltlf%mX%tb7E9ScnKTUP)-9AS%2kwQ}5fB z4WYN3)gF(;q1B7uO~CPVh?2wwwoMqAU6QwwnTeEip6A7yMyk&d!}-dsz5ML*0zH%c z-ctXcsyr%p+Iac*!p`Lz|Gyf$g7K60yTmNZ*jiEVZpujVc}Ge}w~&s<3nF@LYmX`i}Ov+qy& zhF_ygTlMKYnK9>P3NH1c63|sgxTiH21gD0^m9td5aQ*y8tC#9J|A5;6_ugD*2mTce zL0)Jy_YR!>BDedxyqvF`7n76F7B)n*#^I^GAC1$Mw!c?}$~_Tt^n2EOUUXY>kIuc! zY3(_8`V>3#5fn|a)o@fKvrc~|J;D}BrWk+i3VtVi^}Yhg6q=IQruZ=o=j- zjPx=OGZ5Rw7=C*9FFE^HCMgL0mEwJb1Xp?HN6^S3Rp4h~$9G&}t{u?7WrLS`tzR0&L+4{tVLnh<9_U(4bV}_fxDQuVSa=tbOYol9#HsmB-7bC&#A5%|2~be z?-?>;&mhV1hT#wWkNgvVkX0b}28 z*S*XVs+OeJ&kE{>&@VU;9)YOfyL^mJAy{}a!>J`lkDk~U z<@htAqAM_o==(SxBqbmG?~u2uY~FcS1Q6oKbTwe;ESy~B?T^Gc_+gXZumAJTyIRKn zkl1h%(>oDqsf@O7Ql--Srzzaoe(+B!Ti-WR%eRg&MRQO{7GC`fNXwE652;T_m30pP z6lVbO`{1_86&o*^=+*e|M_E}^a1Y(CcF_drz;UR;K1>j1Til~0W4W~w#p4#7a(8wZ zhy#=X89nDtAo*xI&Dy%d5|N#QO2lB&V>8X|a<^CA&?brwM4VXQl2!y%fZ^s*ow2;V z$CEBZJMb*On4+52*zU^R!_fjQt=9WT&yWwVh$e>AHC7t7F&;y+GzW19e$$R+)?62) zD&c-LHMKjg4FHL7UF(jL5-hHhLGXKP)5^o9aEZ2paAO?Fic?+7gydxxs4C5wZswk9u}6h5bW<@R*ENzajzK~io0NTQx+CS z`35;fO0CKxu23#eBK$X~fY>W`^XS0NZ3s3`j6u6*R$z zcknaNn&w`xN(+8$-bZr3y3yx?*~{U-6iy8iCAErCSAMMyzb`ZzwOUSbeC@ zjPbLwG1YrY_i{Be>i)Rgbd88Sv$(&Dlo&I(M z%CU_W=WY3*Op3inRf6caixLkn8eN=BM+?`*8p<_lCRf?9tUFKKr0Gh5hJtS+n<=Q7b~cGu#DT+>Vtjoa}xKaYNfyb<$s~t zuW3A>4~jt2JpYP58?$r-;Uu})t!Ay?`8F6J*f;j^Ec&7kKCxY;T9 zs(DeyRnVfD$^o%>`AQL&%Y~uXh6uwuQR}Q-xUHw~jJ+O#6xt`%C$lfr3u0#Ux=d8^ z@QV0_C#wo7+t}e&->46qeNuRK+Z6`~mJjxu-w}=Fdjm)mHwJ zAzGivEUy~QoOU~Boej%>Ndiy?z?LWQGk5@@GW|+^<5riz9iTJU-uAuB?PSdalOE(? z-93YPiXvsrhimzvO+ObjPY;eN>R$08 z&gVd#R=dh^wF5E~6O&^z;Pk~t(QExuG&-Zz-=5Hr^WhY$HP&24+@R$(6h;h`$%yIz z#RnmteX6<-Iiqcjl%Yy@U&v1~;S|~sGptPg=0b6a?xC&UykA&TR*p%6%?xSVB6_5A zLKAB}kAJE~q15dGw6yOW(h0kj%dX?f&l}a-^K)tW=hc|*)4(ZJjLX9*X&r8>IVtv^ z8s7JTH35ux6pf7!Afm6k8Q${o_@*pL`|r4z=N0%9np|ZKQbKSkss6xtjsLjVu+N59 ziFYA4=ZIyS5MNw)l}a)qv@-H_sB!V87F&rF40Z~}JGUcwU(bmvWI@yD0D_Bn?Fonv z4`f(Z#XMK)$^_wU3JeCUNR76Z8x7i?zcGZD2p{AVS8g6&X??}nsgKxDj@OPh7+JO{ zY_f%un}BVwm}w2_YL_7_;Xs6)5KJeSsj|?i?Id}m6ZPme6)R7mrRpEnoY>B=8M-jZOn$ZSNfm5jNpMhwOxhd-8}-Gm!2rg6|A{SJ+rstygU{r z*xz@nJhQ;fpTI?}&>3W#4Zqbb{Eg6k4DIlq?>&apj%U3urV7q4c+2?EOEx!)bS~Wv zYA@EYb`aIils16ICB)>$?xbyV+1bh2*Z@-;CotR*;m)CX`frV4Xf)88zZ}dB zJ<6i^T&xp1EBNJO?30Tm2b&q~{TXU}QBmiEx?w+~(Xn0R<3pEpGP{rIgEO!Z#)afD zKaOh30)fYkazx+uXpfhLrB}kv=<`3eYaa%HLZ>fW+T4xX-fS@y3Yc|7=pgTtrv}0J z(l)am-{m$Aiv^jydW3CVXg7`2%N@~DNJlljiWDTKh@Ws2ygHkv$E9n#dloeTd3!6E zvI>O><5mOqo;?UBDr`A_I;YoclXPCCjCx+n(2S?$e;B_zq5p zSHmKsA_{A$F$FSh*IzCG!y2`b_IxqIIQ%xylH0bPN)WthEyR<4^d^oX+-JUI{IT{cv#;b5 zd&@;Co|XF0%=S6Me(s6k^4uC{_C9LqW%U7fumC8h0QIh7I`FD80WJOluhpG=d|*w&L$<_kC|P(Nv~OnD5(LKAFxf_ag6&jg$562K6+{)9!z>zR5R*4 zdK+5l^RptU8>ir)PBcaNth{S@R?PfQ#o@_PEFvaDouD$ryTi;6w-`fbv)96VEc?Ul zZgvJxjhLoN3HK3otP@i|0@|{)v}Kcv)jU<%?;p9|PpEFmW1CKYFg-H7FLWaQU=B2< zzUHvv5w-jDzUtR^c8?!Cj1_%i=pR#qT-8V2nMw~U>r^cNgVgUD5TVX12Vf?d81jTO z_Wur4OlxL(UXSV-URvBIoSAhy*w}TeW@v6CL4xTHq{hdugdFSm%h%{5L`@>m_c>MX z7EaLPKLjl(?QMtLiDHa>VJCDP=BMVi0iaT@IBqINGzOJVG>T^`FhltH>?TRxUY_y(Rc|4^8XDH#1)CyCdmUa-x1RuNs2A z^OkEjeScnVD!Plqrgs|91T+`g-C1Qplp5pFP`>|JUSzQltavO-GNx1Vez!8gyCV5d z-Wr=WX}PW`1Yxl*A3VtJi7tKvq{Pk)7$fcr6kz*y9~{4~YJE7G7}5+Md1pQTuM=)% zoQDJFOZbzU*`q{0or?FhAQo|m%p^7MW*Mj^QRTe2e$g6syRGfq-1RNEQm%r6VyV@R z^*-BzKF6nPV;iLnm>P4pl)ct3E;N=yq+Nc15hf_|9 zzW;$^!urzPC7B(l5aVCxffeU!C8rhmVPvEb32)Vz(0x8F$Ja>i{>CRc93IKP{*5;S z>xubtToC9F4n(xulr?i|8@Cc%3ccrkF$xfKx;_|r%jZGnBlfu5!$Rw*wh^uZ&O{ib za!Pt#?wl&^IVl5%6%pCgEec@RWtF8+qJIZ^iROKJPcgI<1%P=ZUmcq0$% zS<1=(@4N(64_|G`(o;a0Q`03e@D#@3BV%IDB|?+*R1*-znt@XPpEmU5M5&&!KWXb- zxp;+);9kx)KY!}^6Wb`S0z(;ilwo894P>(;PuH#&3T{IBk=Q7&JfB!X@gJi@iyzEn zdUj`|uld*(taF88$XKzGX%(7wSX#q{K!ZNR4Di@_;!>KprsSqh1hM1F75~eV z4AL2CSxl{7N*i;@zjsy-qn!5S5JCADC*bPngjYB?YhVoxqahcPaT!pmM+-1H!}#0wKk&VT2|Z;YF+j%Fvn59rc1-PoB^A{7)_F_d9F#q=xO#!S{~zN!E~R7Ze>l`t=_kJvym}GV#S~({ zrgf%IkP@m448QxW1AsX!o#r&^wmeAgYG8k6fq(kCSV)E9(AgzLggfcP;x5-s&kTG! z=j7RV^8rfE&}zm4-9mb7*_2vjx!Y3XcL|5lin~uCu0L?}=Pz?MJ1{6dX4Su&Ecb}BlP`!Hw z?Hin~DE5RZE{!tXULP|8;|Fhns|-EupvbT9sx;Eps3GAlHbS%woi*AX$z5EuUefy_ zKF$v3 z8tIafn6vTw{?0l7b6s5XJp0*u?R(w#XW=fGXE#9NMb$<3PN(M=*ZSv{UP~zsB5h8k zbdooLNW20Ukt@KmeVMtGLqC4?SShmuhHKqEA}R?%V+lgT~hX&f!$cM3vt=q_ubU* zG4;jhSV4agn8Fgi_ni224Jn-r0{25k_x+fYa2#!-Sn-Ad>cRw-xk`b1FOHnfpDEYr z;5P4Sok_7?5gfd&#eIV1_p_m2b=}e)i2HiUN%AR=x--cuA2`e@NQh)x@|P70&0hAP zqLH4MBLmaCwy{6gsNcWmnf~zQ`tZUb_5GQ9$uND$WgjXnwj$^~0~ID)oHOYoj;2sQ zx0UcraTGq<K3ZhqF-c|ksfEx#qin7j;3vPghx(!6w8-kS#mTAe$V--u8 zj-}=s&W-q-bni>rnj_A-c_ooELf}jQwdUF+ZB-$yT*N1SGitUxe}mt&gZU^ir5q_L z^COleH3V(~SW!OnGd4dD3h$@6r|%a(xQJuEr$oK>L*%VB*~OP*YUhLRW4zzILFai# z5qti$U_|+0$clG1co>OItDNtk9x(i^bdV5q3d)(!$Ny2osnbwEJRboIV z+?T6Y+&4>yM>X&z%PzJtD-}mZpF7to*A{}4+<_oR1-|ZT|1Q}u>Zly9nOYb6ACdn$ ziT$=4i=f$WZ|zS06V=&1qhqtVJj1+RCM=3ewgQsZ4LGP58B7-g!hrG!Kr_HPwr>0g zQKkt{X~tKuezQ*?uFlzzFZUo+D8o3we~m#SUO~DV`SR$;IqqW>chHqi)Jfy1sxKPx zVR~_wRv+8XJlws+GM#M(aEu6yClqT3+)H^TrSw+Hy1$g)C+f-ZvGwf=Wx$Q5eK}W0 zd!-|T1ZtEiviRj7lGAfkp|<0#3Q3w%0Z{g^z-giHu1?mqjk1O-h7uAtU)t>4@suWq+iV+EvjsEqwq zMLFj;OmN;i+9gi@`HOx=5OeW&)3@=#o%1P2*{P~hNCf~NY{YxMjOveu1Vf6Xt7L#{ z`Ny>kkS7BY$Jb@F?*OSlS?eRs5mgV*Jpqp8p6E}He7>taRFE{C+l+4S2W@NgPK{}m z>xuarhvS?It5AC(oR4d%ZZm7@KB1*~OzBDoZBlm|;q{+z2{*Lv+VTDhosKjjnvh?- z;7PY5%=I5VuI!53XDDR+G0hqn_&!R1?@2uiK62!&5b_+x!mPi<&=97>ic1k1F zM95#2zqD)t!cnegPtdV2r{$|)l3);|=eQ{UO+(3{VE!EKf+ZHfD31JFwMKia{@(bF zycXkHn^yk;6@Vr&POkVpW4?^p47apT*3PN{WvUxk*ODKpo_C{v_Ynrf(ORK@nKF6h zD0+aEiFHkJmHxd?`LDPA5C2DaZf%X01NA>((=(_KwKQaQ&4di#P1Y9!w>^LZ zb^rY0oQ?#~q3puX zCTwPAdg1*=@ej*ZwTfQpje_GY#x^1{1^th92X-kqimmk@)Et>|JoL=%vzSRr6%ITx~(L7V4jJNxkX9WKnr3n%kh8n4(=SgGUsxi zTAnW-Ul!3>yXD5L>1|E0@O)2@&b_gZ-qXY37rK^M?Vmd+=ZFPJ6hOQNX;?h>@d-|r z`2mzIP9u-}#I$xeJr4RT@IJe*No9v)J*5qW>Q5{kuW}_-#`fiIf}sfLb_`n$wsQq< zXV+%7v7D(J)#mPuERRX%Rk!`zR-{kghz$;?4^dAvSB;EA=;JXsFo5yoMkhgsfb0x7 zf@USwt&Q>w&J@QFgjg8Oz4JRac2G@byPt6%$!Yhh5dbLm@VhWBY*J2&NJaaQ$#hX^ z%VQG$1zMh77w%P2R#5Kl*WuZtF{6nKEcdn&fbZZ!kEU?_3nqrS_Sd5II3jv6!S;+Q z`>2C{uxFqr9df-Xmtrf~Kl8N!HCL~@cFZ}{A-{@EmWQpO7@OHw>i)O~*}V?@k>c|*T^__Y_ha1nT(cZ0_Z zI4Q`fdEI&_LL4}4HK~b%%~z_Cnhf-^hL4m;{uR1sys`fnKfg>jh*i$DnyPj3A`dV9 z1#UiRy@TX@z|Rvy2v?^8BS##+05_#KN$0GnsY@gmMa#LvcSFmP zT0+sm_xJApPCd3>{r+Qn6m}S{8kVSGtjCWm>rytZSdAloXtpzr!y``Y-+h6nlMH3r z)*MNG;g%oxIR6X~o@-%@u4w^6rJvm!NFgEKkS zFn*I12FsF_(ywV-#5l&C|K)eMNnLlQaVIYvhoQ1br4_;6du?lxpE#eHx%pmr(0A}Z zjr89lzQ6Xqy=?xk1Q(Z4yQf1u`%g3yMF1*=6p)>+jnifrHeA!))xg;8GN|6@oxBF; z0J1iG2YB>^Q0~H2bouQ+L8!yD+MeFI5js*#8xu4pOu=QZS^F=}UDQxo=h>SqyX3XD z2;`Uf7U1<#W~(3c3cJJh-nmyd&&n)XW54Q4yYBpoIx8FdAbz>e^~wAQ^o^1Mx}AE{ zDx|cFi2EWG7~?xW%xpHtK&t#gV{t}tY9M5{U3}y3>*xr zn!YJTV3f=3v{&D@*zj|9H||j40Whqk24cZ;=v`OyiW;=(bb}=q7R*JCNAIjE$*}R< zDth$#6|skMW^*FR_G>dD-z~>&J#9Pgh9Kc<59BRM0~+K`!BIRUV!%w0LjcvQ`Yv;q zIg#(IzmY=lpeXpJy#M0qk6Qz!d`J=3XBWHYe+?ZCAHm%2ExDIV+#A-2jr!N4y8|-j&L#UFjaeeXk{`g!ou+=WPNe4ddIGT`e)=1Cw`|4HIq=x+WtY^gAO z6>2NG0Vf!9$GQ5SeI{%v-w``y>~qDl-k{g<%`(tfWms^$O~SeiG%_<%Cz800UCuCl zXCK2i`p&ZsgnnsJAltN@%o9^sT~p^RsrD9sq_HN0I7TN9etRqm75G0OF<7jy2V8V4!F4C?*TO61$k$DHlFLqVc%%LrM9fR9`1K zU-G@-_($%>De(v}xgrKZj}eU7_dMA9QVg)3^x=9w6aJD-PpV1l^AVc$CHjpiNfPKtW#I4cIq*fPM2jf+?}3w&c$T zYPOK^)G!+pYohlX@!j{OS`Du5CRc#B0UXHY%S;{mT;_h5>r(CK0EYzv0ZnjBX@hJ; z)&kid+<5f-u0YhgQkq-M^07R5D2^;iM<)6HG;k=PTx4f-RlgGn8*Ge1C8o`x=3A&8 z(V3OxsJwb_4i@kU3%mJ>q zouY(P|BDaYr3dc%yJPq4o0uMb5ZHj+bL2>DyuLpCw2}j>;EojSrQSa$xRj}D?$sd( zg?`XJK~i|e!ax__9(3^owghxv8-Gr0>-)NY=isir?E5Pr`Q_iEEy&|p6;2>VkWkLfrbvsD~8^fWgyEiq4z^5ihjxLe^DXCh6;nMtILW?ntRzcf4;r|zJg3pwUU z($beDAzWMTfK{=D4s#@d)LboevxIZd>j|0IKV3J>XlFLjpYE|>;zXKYfTmo<3WZ$0 z{g)a!JFu`TrtA>FV*ZeZ-zWb_KquJIP=B7alfoh9b_hS%5f4W4SJZl^TK<33fIHvB z6eT0fC%Z2kQK&c%v8>m(u5~Q>^z@h2Mp)qwbvzj9O+XF+_T7P1f9$R+UTHX0H1eqk zKgI}F6`WU!822Jbd1tFLNr*O^#RF(1;m+OIWG2_ol&_GDSGYj7gaH4rgf!^RTim~U zieoH+SFA(Pg_XL|1EA=R#jn_>gcYiCLiNM=`0bqq}a5b8|o5%n>T*sa?mU%sm|=dG}4SS8`%7ayXX|h5P=&74IBLgp)eR5^#xZh_yLb z0^rYUbXI-{cY*1CYNP!uj}C$+3C6qCt#{9k#d!Y+CpKXxCf0ukfht$y$r%T_QI>ia zF< zv{RlmO0&$P-@G@Uzl#|wC_-FmD+3OtOJW8!{pM&dEwNfw`ih;>2U#V}3xhZ*GB7d+ zv3#{CCMGMb8o2PezvD|s?QlMk9R7Wg$kSm>0b!|AfIRR#Jd;~QEOSna{Kp@Bdi3I? zJy!1?U@usKX_S>59`PMy*>nNfSbL(6K4>F;LPwS#OZ+ReGp?Zui7ci>Hf>ky z(Hb)pXEIx&nnx4Nuy=N^8Gg$rGYkEE*@z_4c~qwPJ#j?;h(97w(x%>1A57WOuFnST z)$Z;>NyCNp$S~*eiVrFrp(QU}1dC2S!hE%hHxhwDfQV}5`7!h9dkrMQ$9UuGuYym1 zc&^(Pb%|m%2hTzQV@pv+brG85M$AdzD1-ZEps{)2>gTR)`uc1MxkKeET z_?DgZrM;8|%etB+?cQ4sS1Ig%SDc{0!9l6pwZ9n->v8Xn{h?D)_YQ5LVJzRGVqdxT zN&n46H6n5gT>1G4l7D}P`Kf(pqp{8E%i}0(aE1iim+I z*H0@ndID3^syIXAxmhAVs&zW3zgOUP6{R(dF`)vV`Yq8LnK?YD^8*8%TPDw>k%~JE zQjV|xuMT(FhY(l2b4yNhoOz@i;qLJ>vib0FlF8ZC4XOlK7eMkT=A>(v0X_sy_~2<0 z=5`Ha`+H7WAd7Abjw;T3?Rda^^qv5K37K{C#uUOmnU{WxzDW&rDPO0)>HqY)h~G3N zsm6LeZsuqo&ko!Ok4%3&p9K+PX=9%jw&a5qUs~ni!D5?c=?Z?6UEm@k#7y}_$6ly+WvjQSXmUp&cmPX%E)pM$sz3;h1d zk!`tC}k(J()hX8GSP**jN$Y3R`9YUApSLVYqD)(FXl8F4&|-(TqGI=-#W zef#1vrVR&1niL@UIppj-J5RP)gcgcuXHnE<#>22_S@P(_F=r*jXZ_ z;3K;N=@j&jdwRAkL~^;jqh{{w6ih7Uhyb9WON{#>CG#HIsnl_BF>2Jar!DMA^ZxB)F>bx3oBd`YSP z)0Rt)k0##TeZ2^VlE&pl7TzR`-I|`5T3Ok}Kzg4sgRxfOYpaz_VdI=>TRO0v2WMAz z`%83t-PUv~Op<{uZk~wv`p>QukTf8@qZ2ac4Lf2Yf#Ks{WlKq>7Ve!Hs2Pa$paN@- zJ~DhRN{iFS?u11Gp-D|PkFPxC_nVM0x8Yu(K;>irRWvj-so~&->WHD;>^s|b#2lji!KRDRpMhP{>PPVCG;a+R=NtjY*AD? zg_s`#*E=EG!*&^9+@_jq0K+1mnXC(4o}&T9Rmy2W&&;nR{l|^XQnO1{0T>l|OP?fr zJ=9g_&AT{!XZ0j0OETF2;2Q8th+62$$8IAVGqZ(hgC8(I?Wy_|Em((C;k-qjPAsjm zIW58Y@Su$i!OJUTqV+salRAa8^OeRyEbNlHaBeYA+7pO?cE2aRcRI5u zwTzs9AXi%xF6#3tTW0=n%bEtF+iPib0=^OBhOPVR>2R)n#ES@`s*DIF@hH+N>TLY(HoZU6_mlt(&FBt#21a;OWLw!)Lu^ZN zZ(7AnsUO42xk8efw9bC)&5xndb}OnYH<_oB<0odZYfixh#2T$X)``GZyvm z_He!kK$Vx+u&bp~1Sm5!(z}jVawfT$HKeuvs z9nK30xTX%d5{==gJWfOa%46vs`t4d*hJyi{j5pXuE`b7$0KR+p_ypTFPHA}i9WvHO zNAn>Y#SOLOl6fzU&9&@0Cw4upwktJucZI2P5_|IfxTxEhwZ!}7U%!=j5{fYl0A`r} z*yp$NHbceWLZ56IJ3xu7yt24>mkcSmhi~BSHgD-~nSz|~PD!>BLjIpd=)VRVaL9Ue zyrf@ptsj?`=Nr%5q14uEms2xH^Rw}Wz+OPHUB)xk@JVuN`qx5DDAO#|6_7q)LQVKx zAexy0rqY6G^L~#PZQ_D9nZ<0HUnC@i56CgldR913L^n+;^Ot?UU;7hF~4NJUj9v zF3`ULL@$b5s52nFSIDaK9F2Hmn{>7diEZ4JFA(|A7x&cybJL|nY8y`MN{br|v{D@A z2(O$6Oa(;dFs7M%_ASm;GX`tbhGX_(W<09C;e5cAeM2PPRuksde4c&dvLJ=d^#GG7 z`^l%v+X1t{_w(85Im7Y6~Svsy9egItfr;xdKigrgrf z6TH?55|1mnEJex7n--R@&XGH0xd>!}WR<_vx`}(3y%!H%>GsJ{=$k(`rqj}WlKPwc z3R;}q6Mg)e4@P!9?9_{BmN#1o+#OX&L`w$eK2}=@u$J3aPj+a&D57>(nwr`7D%ixB zra9BMePd*HuaxPOR(^l~QZm+Jm*7}BXJqVv#szUpDdTYSp%&v7@W{{t=`&++T&~Jn zl_BN@23Z6BwF(-$V{DFX#8g#-2&TN{3-6XQlLTnp5f7C$l(QD;y1aDU=?)CZxqQ^8 zv%V41Zz=LzhrW>DhuzCslt!BGrObbklA42F3hN~EzAR0w$XjJnip0O}hu)hc&xHh6 z#oXSazC9>KAQN{S4 zYUe$^8ALnhsli6xIUIVNhjvntn0I7ifJhKbv>4T$7PZwVrCB~R588N|W>6DvgO=Ke zxm^)`xt7z}>ryF->uTQO!uJ~FeW54IWqNhPvPaNWy!tX?r+ueNRbl6bZ;9o~pQ8RA zalCH)ZAsK0WzbS0Y=m#EXKTB!+Io*qD7(Qf5|$^h;U?rm$eG7*5;8bT9jmI1TRjS= zQnTmz#@TSQKOum+|C)Qqiy8)bPO?E9S|SYZ=jb;qmEO9t^{d6nWQvkI966vnHgi-# zLt;FAfGSHHaqPViUFU`yzEO3k!Kdf{x7nvXbMH zRa~qxVZYIxhvi?9SJX9Z=*rd+^8+p-DQ$cAR~FI8u-4q{_XVgw{#Yq`L8LLN6&+ElZU1iq%Kht8FA%w z_P7We;L$#u!>eC=Nv)5it&1!2s1vxZ8ge=>&I@b0qT^Sd)Sg|8wl`Z0?#W1WBJc5+ zmW7pgQZ{OfDc0yb^%*u3<=XYNnMX?3X0zEvjHbT<9(*wIN^8DAn3Sz$VU4xF+v-Eh z28EDvG_DEmyke~2!>+PU_82`+&&eO9MXJ~@q7XWht6!l5A)giC+HzDU6oR&HL`=h8 z<z&XO<5`O&Cu%8`YZdxqLDrtdi_Qa2`87 zJ->o|4(&?!KZ`&TpHlcIx%fIKz)IpH>4wzZNFo*$Wu?FI#<0sXXp)a?|FKnLAa^kn zaH8E4Sn6v(tmbDw#wW+b)G(#cnC5$~`GOpyB$aZmiN_c!nFQno^Bj06D?ZKSZ{mH{ zn~y)W{`9>1n#8C^{PzYhu8h|d9XW0>pFVhw@}Quv<@G#;5PaLFC~>WKnn^xj?t3C* z6k60IH5hX0Sbgy}quH!|H|O2*()1Zp<1~`XSX8l8r>``b&B-8wOQ)KaMlZ^ok9zEY z?l5OEpq+b3^0Y+;GchdLIoV`mtdDU65`|SM#4;FB2dGFN*Kmlj@MB`(?#|J5lm{bt zp_1INmjz#j13@-8>9cwbgf%K}^nKL}RIBwl7CQG0#Tk^8HhnMgaXW1C)F_E=weOeu zoEs0lv^(rd@v><7XU5rr6+>26h2~H_x|-HlQ>w%P!%X5~Wi`J~EPNQ6eY8d{lfjN# z;To**a8l&$aZ9sJ?-}wcKqAkqMPiWV`wy{4o7>Zndr5zlBxE(OUKB~5KZ{lv#~iAp z9^#uC8OOs?H84{C22}M2QFE)=+5GR{C90W4nFFfAfm-*yILcw;CF|z5J#LaJ)Y#88 z!*wE<2U!yY0?t3F8g!R1M9D=>{y8V~ssA|`J9`kZX9}Dq7$_@BA5rSA8jPrQMwlM& zUnKkbJ$viz-7&jK0EGjit}0ag2@<_sW}MgUW?R6$s;0O8dn2Ox;#n;n=|Xo9!<@u@ zaA|K|d9sx_b{pn1jO^z%vJP;hU9L|ZzK*l~qoxKeo z^osmt7V4I{birz;Gw+vk_?kkTN#kHsDvaM7y91NS1T!+);b7ai< zUN5%}PrD2U96^uM_I4Nu=X~$>6b_u8F^6v_%QC&vA0!9(m9#1ch~*gsYj9nG;{Ddv zDlA{i7Q5bmdU-zJP?QW2{PK@bViL-eP%ZUSGY)x%D2&Oalo~g; z#4kGOK|$#Il6nZm!M9r&kZZgT^x-Jsvt)MMLt60#>9_l3N1OXgBkt|F5<&sU;qPDi z`C|>#!0_d|MQqfkW_b78uNDyB&=qa(lWB+Sd1Z)n7xJJ{rzK|G(C|<{2jbqmZLFa^ zu5#q=lp9)ePSC z6^Usm*gfoRf=-k^^5Ju&FlJWeaG>mKY1W=l;8Vp?dK^X+oK8L=xbPXKAQ-B_h(oQPTv<1@xm4QJp1z;+V~@^l z4k@!D{+J9*4NAP1DaLQ%?AG!4w=7IyB`keyCCfc4BS+`U=`d=#=0U=oF?h%2lI`NriR6MHRyFw@q%a z3-eiW0ke`a36@c5rg`13FB^n~V+1Y-^}Y9{QrNs3uFIuqTVJh(E#6Dk_ziYUYrZQ!LFZLT!UTUSDgrg zsa~O-HLOKTGm2|x+3dLbF*a!nug2}MUlQ+?8uoQwpR7)YO z5puIxw1acsgcqHVat{BkSdB28xZ{i)ZYSYd@4AMeE|Pb*-L*)l zwaM$PHvKm>xaK6E73E)yD!Fx$XAVX#w(`N|c24e|s_~Ne&FT_DuIGtzS@{pY_h3YD zjgRPD{ROYU;jn=U3F4&e>c;vo+?B-G1-SJ=;o~xRd>o(%vQ2L)z?~#pgFa zLD^T@V}^NTztC_BXm6Vdo zcUG5?O4U3<#@=}Hl<`1{lh`b0D?j;HeziQi;L5F0pz1v0`|@l57-`thTUJPV@uc!* zss5*ODWuINn^}oR|2n*`fTdB5EBKf_d(IC$MavaEE`7iw+J;Bz|DZ-JciQfm3wgB;=JTRTWmof z3G+mh;{;pw%Vp}6J!(Ql5o?*G$?!*0ekC1JBiVO_1H*8$+ zONoj4OZ@NZ;Vn++J$tQo^b87KFOLeX#&Ze&D;>QDn!)945YGZARRT~Ttul46Iw!Hq%`_{LfzeWDVv${fx z5`Er?^`4luRMM?riu_h%N#$N~GT3ykXBpnd0@l>y??d2fA)=^-gEin?R@Ix9-Hr+O zBs+eT>UVS5cK({SiwqL+Oh-HG)MV#RA$rvFz$Gt{qCFn~q?Iv7ViKxqM1U2K_W?c2 z#rG{JWTR*o>KllAFXZ=`=m~G4Ec-NHP=|W_xB(w3KB)B-8wQ@JNsGT-m*q<^Yq3~z zROZO!fA0E%I;133+;@Q|i$e_6E6jgLXzzDc^2pAgdZbwpT}^}eWBD+A zZx2gG%3I_Awzy$p7`UE(BdZUv3W28?1k8ymT|o^9W+K*F{5r4Cz1Gw5d0dZtrqRU=17J3R+sS5-v55O2=?hH_(jWjQsRRD>B?{0ce&sain_fSj_y_=A)er{QtX7dCur8HP@mBx(*xQ06ac;7JFDJNS! zH}6qxQYOdX5)!H_m>_ULl($|6;nr@j3)U;7*KdKt1v_~POSyHe~@c#9vyUkZjbBHf25Wat&{Lw#(!u3lafz! z-5}fqjBV%=%Qzsq@)oku{5@KYWjSD*TyL)b3j|OL8PRZZZX*Ls+5F0@TH#MWvb!X% z;*d>i{%zpBzT$Qv*yGY%#%T}Ksnf7m-b1Ow^3B{6T@`IJ$gn9=okjK2Y|i~yE=KD^ zA=+M<_GR=ytWo zpqXesIPRLunuf1_iX~}6VI7Cj7o^`ycr!(v+!4h0s z{M>n8P=h)|PjGwdEY7;qS}O{#&{gQYPCDtElD#_7mu?MuLThF%zFAUE!8WNmIr5k> zTuOLZwr+0K4#}I`+~(xv4+r@g3hO0myaFGkBp74|(}`zR#iH}zd7?*!k!}um7_`Ti zYM_Z|NL}cj!RT>5;GndfJoB-=M4YjFH`d=AxW<2KrYKm;F@FZFyjlM3-Sc;n2YEEg zd#P62eOQf-3_NZ*F72~#^jH%(!K<3qoNT!;Gtyfjw9R|3d9l(;^p>vV$=C4CF%5mAtbM+JGp2(TrgUI@WuoUO746Z$30s7m{7* z`a~ze@WB6z5=o$)$;BOT?aA^?W{)ZF8reIAyF9b zRSCYvoJ;NA=`aWyqs${A^8jXf8_vlErUbVWmlU7|rWs)F;7_I#&ZI`#Mw8p1ejN>0 z7NlhOk@{+S-dV#+Jn(SK6vaQw#oZ>Mv$_rJ0Y#8D3bpk7*FVY49d|{?#Z4s)I7%wG zeo)#NB+vcy+*S|#ax|}G$hEJiy(;R=kIU7y?r1}DVJzT;%{QxB;Ak}Q%ImN3k8m8Y z2=3%nKU^T+(YZ&f^hZt~)FJ@cyej+0UYsg++H)_VEv~s+V^B`w;;%X|~xIlzv`S$bRcDdYx zdcQxVqxt$GmvG!mn&_p@wR3?7STUrAHl=e96{G*H`wxe`xVwEV$Gkx)a5tZiA(s@n ze6k3(G7D(!sOpg`k*AQTwYz<6v)><{=D{=a%-X#rgdK9}dt=w>UJQ;YiyAXw%8m@Wvo=Nt`_P11eqhncn29lF>YjcY>Ask8DhPD;qC_ z6^M2Fw7*&D%;QrHVmU%Rw(FJ@S=X9|SZ#g~?9AuWnE3_ityhsB*yRm&lvaG(DLGI7 zlN3eeOKF00^{ywYiCf42erJ!rFm4LhJ31f2_f^kfscsz11kYx(qz5@L(F0tn+l>SX z4TKdC!_VwrlFSjfFq*kH#)8GDQ>JK-yUz&jRCk(-zSxe>SA3eKlE%K(kSgsy{-(nt!ozkX5owgqZ*^~=K%xuL6X{8JVNk$VhoCLxcIYv^^8>FhClPg_R8 zXTM*n=71D9_0y)eowjMQF6?A^0&v^c1TTZBWYr=}gFbN83Gp^iq%SOL zXQFhot_`^cJ)Xe_c1Zg{=HYgm8CxjXITtP7LwKq0KfS!)Be3u}#_uhLCscDrlb(PU z=d$sT@n5aSmNm zxART412WV($#e}ym6BSd3+e$;>PP9pH}x+l^$SW_sAg`Lz?jE>+x(B)R3O)p>VkS| z1N)NsOTA8Dx5y^55(VqMNj+=CHmJs}EbE>ov}-lr&;r%&%GaoG+tv{WJxphtqk{jc zcXrZ%sGDCmNQ~C&>^X+F^7BA+sddf5R+Xd^?LH!X@ycbgGvf8KqO$y>JzKal1bZyFf}j4~&sn)huq6f7NfuR>I=K zV>*W!vJwp~iO^ z%H|QR&1{S)LB%8(lV))B#oze1bS1n8QDUV(HM4WRMZbsgzq+Tb_gP|S(4W+l0g7Dq z4)JGZG_G5fFRcwBr6HFgn z#ov4RN5TSBc@0hN(Pr;MLi9Zzs82d+E`3@X9%wET1Y%(*YX2y{yYCxNf)^dls~8&5 z>U?aAQ7=RqYfTP4& zX#@n+(_%jMAyf>q6aih3So`MHCJxw*E>^PhEAR>Iq@-r#$a9brB+N4!yWn+P%=jKq z(9*b`--<6eUuT-c_2k~OJsD2@Y@WA9EEOMk{C42&lz+snM}*r{*+%MjP52 zr_<1_LDwYF)Y9B>xYw7Wr!NjB$am=_#M-N9u?rk(aAekWxCRTSJJk*EHP|=K_$iLv z%H-jUY^2tB1(9m8MaU^@rqa#d*a{h=HakOC%biM}QUv{qz$;MBTS!xmKL`IF=)|4v z4+U%9FkD?bm!6L<5~>hQ>W2m~WB?U=VX@)Q?%`|e&o9eyTq5#o<$91UE6m}pn4ZW-{Z_zDy+4HpbPs&NsJX| z<>26OGMU$GF-H?|gXFNBCvd5&>+U}i+jbZHOxjDIMzShj9Tkl(QpJ@~&63#9UwlM; z;u|qHo=(8$P$BU3L8y}u>xd+2sU^5|@ulC~Jez#-)@N9#LyhTwrOmZtjB4N29{126 z^vpp-SnO3$jr2`x_gdQW8sqQZ-Ys2cv{3ljos<{>R4*!BU9iwQ^%K~X3+>F`>#XJW zlShuGGpqeRKZhe-zyW{t1ah6Da3p|>;|Q%(y>$`+K6a0UUHUXRIepl6WO9M(om~Bamp0$iUIiuO#L{ zfk8Lu6*n4rvLofganW9pr2o$N7^W!xe0`ney{vbBfm66iKGhJe2JtK>D5rWLQwGol zT4DJjc2{Y;C%Yc!mx`rtONCuYEZXBdQp;<94mSJznR1v$7pUNrIc1d9AcVu~IypTz zZFYi_9%7NR_U0!Q;c$Bj7`QRx=-GpVUTmCC%M}ej8sRi+aapelC)sH8`Tdef^GHcD zsP}pBXt*4V7 zY?BQ&k3#Oqk9D*+!cg!%}SE< z#iTm3J6&y6qtA=IDffXUIe0#)q9P*;&_QZ5zg7ZL?!* zuQXm=t{Ha8APjvpq>pCPE1=P^ur_ofe5Z`NoyZK@lzrKW^_h56o?lFE@6xsIM50ZL zuQxF)@^_csWm=Sx0F5V=)3B7)5$d;%c?>T3Wp#ryPO?QLYb7-LA2p`BBux$`DbZ3i z=%<%tR!9Fna+bb+T)X?81ZgTuv@FXuzc{ll2Wg{xMvWUkYA9r^U z*r%H0Qsh{5My`|2))mF zJy1u;TT$bC7dH*wuWLyYtoqF!%*>(`-}cjz%?YK&hC+YyUZ$m3ruok=aKTcumXg8k z&&nJzI6-FBv0Z2_TT)?4JE{D^nzXXhp!00&A4%sGptN+q^Njbu6>+w@Y;BX#nMYpD z=)txb2Ay4n`q87u#9%o%9UO!2zt{Q30}a`S-d|T-b~JX{KQK7>nFK%d-WEI8H}=HU zqg~Gf3TndoE3;^zfu*!Gv*|b5+FmhD&fZT}i-wS|zxP{|xxFDdt@6=`Ux#DBL@lhH zLW`3yYu~185u^UkW|UV=dCxz`F$YS*mSZEL3R?afSFyJd+;zMmePmghe9JdywBW42 zrz%Yu_bK(&A8AU7rasr_r8*1|XyRGY(SOW1(@^SSyu`+P3+?Map#;hpGd#Bsfy6E` z+}xl$E&6xFiZ|y!uKi!z2xH~M?KQ-ScGvZ&{<*)TjFEjwmFi~L$&n3#{c&8yfNyAf zuuen2##8|Rua;)#MAMDqW5!=^C@j?iPM~j|r04AMiF?U{9`{zjo4&N4d6!zVY z$~#m`(_{05EU5%@Y;%zN@51KK=;RqDDG3N;y{mt=8^+$IJ67DtXn`F;y}kyBUHPgt zkX)mf3UK;&VmheHu_dKPrF%}gwEnX7{83$*R}_x&HvUoMO&mmwHpJc_Q<%vn1!;to zQrKn$2Mzk@a=9 z$?(yVK;9?Q?rso3_P?D{JUDvqtjc|OV47^Y(Y3+Sx53_wsjOg!B$3GT;X?V%$4o_V zr+C!OHpgWk(J1QBch7*g;}>vxy|&Z&ZFF#CW1YsP@^DOuDTe9$)KJBgtwtHma(%C- zlp=XO@l|H*0;m9lv8d$P5A>JjH#|79Bkml6j3pZren1(1YZrpnqM`+XYbV1EO{2|}^Dk9~zLsW-O{ z&YCUEe|DOGzdMa7;mE`nz;+rDeE1)TEt@73X@4F!mmTXbb)!$#dHp}Ey;WG$-TOUE zm!yu;r6?&44h^CpU5bE6NQX2?4@yZR-H4P52+||nQZpdZog*NPgbd8PM}5A(llSm{ z;)TF9d}8lA*1gu+r|VM!CuXX(n7*$^nD!bqFg{p_W~X248YzZ1Wpyijrvt7r94eDL zJMPDAMDz1!@ENomMOXiO<^^+yIIE`}^onb3wlUxSuMXhG)&UeY1`eV>06z;*?dCSI zB${nUp?3&(GB&!cePX#oCz-xTy@Q2AmVf}P;|uDA{)PujxyY%A>d+p zs=Ubl3}e0eNI!*t>Bd;~H{)tC$=Qqlb^HAt3^hiC4!05M!?MTujI%8P ziKpI84#50O3AfkfYyPk2ffYyNj?;>yXg|t(L0HJkIqIsZg;TAj0l#?-fPV7D-jStI zzLj6S^TYRH&=U&-@=YupUc5j+$AE0A?&{M?cjIoC$ef?!J&$-2rKu@Ol208Jh>0hT z12w_UE@&NZ(+Q01=HA&(9JDK?ERci7pRgE(EV*PX+v z&^!V&28AXi@kL-RM|S(72g&y0?n*kTVS)<^aMfxHP#8Ook)g%3S+C4wmoOkUVz|l= zPCJLM2BP718A43wd{rWkoCdNi{9gFB1-+nRG(M^_$c!uTZ$!#{@$SW#G_UpG+A?M~ zpcjTsKd*w%^J(Lq7+2hW!Psal3+#%+qKBs*g+_y+2jrd<8IgJd#DSv5K2#SX1coC= zWnh9dBjZ%zO}A_;4FRMK};Mz<5z)*n`(hfxBSDtR-nRmPFJ=i zRSUUtSv8&9>$-#blwUJb5jpONp7!Rys9(?2gP)d@-=6>2++EWTQo(n@v4kSKxr3c3 z#$Uw{sEsQkp@=@)D{lIjb+2gRB6oYqiUUvDcI)%K+qvs9WS<|tZ;Y#ZpZgr5`XlQm zS68Rb*LNQ(N9?t@=>%7swy#}cR{H?C3=P*krGPb;qgPOX*`^_TcgXJtHg`Rb+L1e< zpFYx##Gqo{kN_e_RF^ra#UV%I_MTA@wa~&!*_TU-OG7luI@|Bl?@G0{TKcN3C;kzfe%j^j8 zDKm_1V%@m4yr}TP@5V8S$Ms}|ng?;;KQZt7y+(<#K1Vr9fh2&uE^kT&ADvsv$VUA{ z^WBOy;~2V-W)lN80i{h7{fiPKkFp@Z%f zSG%o-GbSE534>d|dB*=H^IuBLgVHQmzX*@&xU94q(9QsbN?W@7#bK+*w(}MBT!xmUz}QYuWXkyJwLL z)lNIE>-J9m>r;MuSBUT(xsG2O^x|F&*u4L%)=)Z9#|Xd-F%k##@pbjlFxgz|(&s;S z$0m+I+IJVgW7-HI-jXrl%Vu|r<>;-#k>!y->fOor7jbJxpka z!w-=K^MpZsekOULWuO|y;;g`;YfUTMXxe{+X z7sqSBaQg|CvCH$Qy0ME$WH;>@QE;22rsV`pt z+sV~}g7j3enc(zjCMM>pnx-D);TYsOrrc#~|8k*j2&>^m6eXIG)hxL6U4FcW;L3Ke zSm2QFW6*iDS~C15pWO0AHe% zQD8a+GB@OlG*boSF5SC?kY7NRIEbxV3s9yf+0XovNL}h0disYAt=DX{#}X(K+6jUZ zaWiEP3RG0r*kkea+YPN>6i-mb*BUTCBmvR`awQAA9=sq$!wx{TrW&jBZaC+cv19%_ z`KQj2q>;Cok+ff6VcyFvVI+6-`rUNAXETiluUWnRS9s+tb<7S~N|&<4 zXV^_#FwcK%?C#t5!BRIx6Zme6?(Vo6=s|j*4(q>;-~oUPYP9p(>VWOs&dx0zu?6Rs zau;NV#}()1r!6JQgyo*x_?YBsq=6@XX zMrb7m0EPfA+B=A;dQgxf>bh| zRTt^<)AX<(P;@*u0u-!z5l6S1AN2UUIPALff87oo^5z74Wa%#(N5T6OA|fA@_W;KH zy?Pa(@pKMKq(2B_Ef}wwAVAezV5nOBx51r1!Payg-W1HRfsQd2wU3cPK8!&%@X$NF zL)}?O{bQ&DY2Qs%*UtM-Z1P7728Y+LT_KBo*!_HbZNf_TJxQ+b`Oi)E7hbgn>V3A^ zsA2;Cw9?h<$29T_dyigH8KJC45UME!ieEyfBNrJT4+c^Qx zmjAZtTP9^3OFN~MLhr~LEJ^qs33DhpB_Ii7hC7c0i0f=j34VxJmP}iVr2!j|E6LYI zA$;l|&{Ty(bBTrYN)G%z2h~zf879{yaT}MuXA`y`+{8OPjDIqC`%{JlVgPB@FJ$k5 zZ@MDeC#T=WWziwg?`B|Qu8N%+ib7XpW*5E|Rf_+c8KAa}ANZsZUo?3vUv>wR%sCOJ zTlqS$u9&;B61j%B>uigNV6izzG@z3q(@BQ*dP?guOD80gq3vfgVWtJV_dggHs4DTN zbCINFXD%B3BA=cH+-2$zEQG5v2{D6*i0~e58R&Wh`#BqHg@&cf=ukS}y5ApyWRH(5_laFP)N&Bv+XL zIz-C))6pf68e9NCtQTgu%tr9FF8tp@sp6^u`% zol&hSQ>&A!f_Z8nakw6>tqv30`qdIgw@mM4GK z2|`xtJSY_4`%bQO{T|>1aO=>yl=-G8m8jj2qNb7ew%=V=58CDzP_jwS%v2yuaZF3F z?o#*F;gog%Nwhl$$ilpP>T{HWgq;7o5IGPf_saf8N%*N@^w^bYSe{%aP_nuA!!lg! zVXfJIn~jr6OQv75ah}H-I*8T3cL+gf@(mdAk`0JtLill#RU*Ah9;7L3)^llqk1Ob^nL!M)voMjq+(Xhq3_wum$P_m2U!hURJ<;qi@TY9 zi`O?ymkFo~pbowMZQ|DQyrDw3Xm2Jhy(%02>1Ah?K*}cl6k8mfnmH#%*@eO<8C(ma zM>0fJl=Z|P;yxD(+-DIs%GWlqFz^sVh-O3ojlKvL^|FrLreVLtoQ$7>s z2P1heSTRATFOgxfqAGAd*mKLX)B@|p4oe1K@`;V=Ke;hQNGXw{%j$6!R(7J4K)yGp zSC-=QIWRVRR-8g$3#WbLmo8M0^ti#V(HM*iObFS4v;5I87uu8QFr9;2mHXo=NW}w< z)%aksV9S$s&KnOzG3`yAF(v&2+1WjkYLWv{n5Ay{RbpA2+hew2c6ZB10OIC3H*+Q- z13#q=q%u(qw_vG01_zr(j{yr>rTip*|7Q3xivzh1{N4wqsug=j(M%5xI&JShu!QT5 zY7JETZ3|t;7P2z`M4+Im6t-A^eF5df&d(~&|HfgjcF%^>moQ&mq^w4v(zDBG?$dZ{ z39BpVBROGba}&<8=yCEdI3o@yXVI<~Y6 zs|kzJ$qp@A)Ev7mRvGtqgA**?up`3%^ue&5jjb9uNl1no$>5d>6$FQX+H6;UlNNB= za{>FxCCdhHf&rX0OLb+_g~l^VH`;L^$5ND@jfCmaL6vcNZeDZpo~%YA6gQ;!0Rgm{ z^jPxLBglyR7QIym1VzcXwKpu=R~gMk{(hV%X(>K5Wm$g=BBb$mPDCxc;c{n){zWVU zAIrTY)tjaLS8@TJdCyhR3uZxPXcSjxKT z63gcDcHeT(zj`$hGg+zAti*I*yNp@_5SwXQemKW}-74Hoa(#`dgUazGSA$E1asQ_eBAis z#|&kh;ia6RRYX@q7i0iy+NbJ(vWZP0*0tS+T`0$M7FE2t^Ucvqn@oK{usiyNypY^P z#OazPHwU*0Pg_d+$XU*W8*yXJ=^fDw=4kgHfU@~?hGprq+H*m5sH}9iA2L0Z{-RCI zbS?My<`4BXM>23Vo3HWZsbz2A14P9+)BPN$Ow2q!T*hiHj?!mDrZYzFhMG(`G4Txi z`1PI4oeBXvOgrkf30MnM%56dB3%iCyD>qKbd<^MhfH*{ho`tEh7T%#HKA-oT$oqp~ zNHGmzxa6XfLu!c^WyyE0p1b*;IYXuk{JHf$#tYi52&Y8Tv!|0TBh%Sq z6+unqo-$yg)!8lW_07QHbv_9#G&VNYr@FR_Y7B;`m0`QmU%8tUg#)-3gH06tMOUru ztyGhR5p>@~GplQ*KX?VXUXrJVwhz4wKMz)9DSLk4b(YFxL=z9>L^+-Xgy2DOp0h0q zPzCmPkMW;{D_iC>=Ip)P{*LstKim-w6YV#cpIHo_HT_0A-Qv>6(S0yqv9{xs(VK!4-kHc?-X~w&7u%oi;)^o1pAMK#GXT1*ZRbra9ztCyh#i>rNXH#&^dV zls)R%-@V5MW#tV0!e^Bjg9OP)@sHtSNAvKxj-a=Ye2Amd*4|i0?q>_C-lQa+cG@^s z!3E68LQ4g_CcK8>al`>Pqq=~!7kWpmdu4~(MnYP0t!?U>+$4q~fV1l#GQ#G6>$gIC z+?ST6iz%Wl%{UH~{Z~Y?2YjAlZZs?FDEjriAY!UJC+5db!AjD4NZ@Vji(VNjM z;{2eFR~H<=ZsJM(6$FCN6Gb5+UAq%ttKv=akX<`@Vv0Y3Z2E$v5U%%)0LoQ?sULD8p&4fw&wN z%Xc6;{nTkbpc8^!84}dB_(D|<$ zeNEeIqfUA;Amd3_v01_rsi zBQ-DV#HDc3TFs;BQ}=T>=bd8|lusKEvzxBT&73gS0>Zp^_b!>&47=RK2_r*{_?V#w zZu&e-=%X4?DeWs%Cnczc9+TcO+-Du3jxIypU%x#0Q=&~Wqy**$y%(6wuD+f zcMreAdVY0!Tb{44y(;3BVxmi3Q-6#Zi-V|Lie@uLj*63W_yhpy)2OvAW%T=FHQ%<1 z*KPP9^Z@SUt6l%sH~^F|l;#rBD}R>V#t>5@E|+#5JL>Kj?1i}8MWf^(Ig91SuH42h zmqe1cZ;|oJ*+jfwG?~JI8hU{#9VrW0JNCmc-{6d{34=zuAlzKMJM+N_jYenK+eLq{f$juh^iA~a`za19*$_D%&~i*qqkv>(j%60Dwg<*MyZ$$nc@@%x&)HUs zdv>ZmH(~Ql`Z4R5?9;CzdEd+^3{yTg!#k)NT9R7j7C+n&%kgYLYK_kq4m+Z{!iRHz zAL()vjs@xd6rXBr`V~8I5sD%zH{*n^gir4b+14sJROIe>7u6`y4-O9pVWFj&^W;AQ zZIU1;4#8pswv+a(GyGvtI(p9>`sMVoZJAP?xY=_SIw(`op`Xn5jOp@Vx6~n+NlABg zsezJ4^o60W*vtc*p{chuOUu6LTU@j^`h_Wvs0`;PR?ny~ywlqc$gVEJmq8b9@Ax_6 zk&vo+^~Dg~WJ|-3#|9Si3K`ZOQUS8{2k~`&E$ZV`J#vxBs4@J zNZ0opB~DyNZ?*0{9eIOD+=+>aMQ}}*q_$tH_YSt~q8G0^HYi?%pJ6yRGo|SAL2ua3 z=H2*0b^~+AsZc0IkkS?L8RqgeSeX%R3A4lsd$D3Bg2mnuD&4?u7z(QKx*r~A94diT zxl%=dt?nzmun5MX$AMkVYTz093*Ez6_5l&bV~f^t2b@p6q9}p0TpP4!dgi*APyn#n zKiQx`5p>Kb6M~6LaVNuLQa{C};tLY-U`*s^4bo2Vy?7r=N=lzy+eB7Rr80}9A{uZN znryeh>J8~F#&@h5a!pbbDuiBlxfWN91xvSYG=n}EyjIh%ONAXl#AQ$Ub-QAj_+M7Z zJ6Y6rQ-@G$AD*2_8_zGD#RYbQMM7xa4hB~6hThX^@NKzv|ZloZF_n7up6*dgz;%8 zeCVyF*lAl{|8)%aM6t_loU4LgQ=krsgEgV$Fe5O5>z~;m58`ck(kZPS25VZLjkEN=ms2=tptNT}P+By*9#QBENVbW zzZJ(`9KHTRFXwaU&N3-$vSOof@(_#D4n9o@2yHX_s!-h1M7-HJdwvz03c+j5rMANb zkD0nC_J}N7;bYT`E?K+HtaAFh6zGL>V6ryjMPiFu4~3-`s76R~>3HMh!EdLDr|(-T zT2%TV$is~>+URbSO)qi|#53&EuCZcW|6YW_S_g&!C?)p&r zGqJLLDr?gf%rw0JO!Ps_;TX8#e@~Y1UC>7BUUvF8yxRXIyy_BlXoX<^aU%-my=!Vq zp!{I{I!UgJmXCpIAG#zJBarnpZB_Th8`yaXCiu9LC_N<`kn?w?uY(W;Ua_9e`y}EN z=TQ?2uwj;*W)>z8hkagL2^&LuW&0(@Ewy)R^x4&9 zIu_%BZL3SXS&X@PU|8yMRbxv-+e=Niez4?N4au|BRMDp!^EOuN(m27(xJC{Ny*u|KrS>2A6hx>@oa9I2hG#>bNCUEb2Az0d#0O#;+! z>)aQ-_&s-c%3bW%?>e15&fy#UT6wr9R}3N3HdM+e03)UQ$OFJcYG%RmL~m3-&SLEL z-&q=mzObfrxXq)cl8`hIN0!HT;~+~Qk|CD^&R(L`@-fBK{|4|hiJbYm61j|o*tTC zzR6c*I;r9cORJq?D4F+ahvX7O*#ZgxKf{e`b`f8LRh&2Cw0?f z@Fu*MLRGEIPQA|6O8HH}kWYcP`|;@<=6QdS+}?4ruGxWl5RhUEU+Q^7fKP@zHh1zk zz?8W=yKMiIHV|<|c<*rf*`UW&xs!Aa93);?r@(3tiFoZEy}}I@=#~lruNYu(rN+{6 zr1;vWQkLl76=T}vj||}6InEDF@`MHV7YNKlK+ObpDyORFj}xU+EYVnpzsu54HN~>P zF!DIg`5boKdOSf9|K=uK@2zpHkC1f7)|&X)WpXa*;x5djd1vk?bO0hdvxiP2CE5`K znvWy;tvqzOjA6&K zb*ID1zHxqHNqY)61ZAh?Q!mK3$H{Ao+E}%Er>mY)Kpx&(aDu51SZV43lM>^1`uv~4 za;<&h5pO2HxLzPEh+dz0?7bRVl4N85egL%-FKSVWW0Ow2U&9Plh7f|G-zVV?zuD(B z;Nw~T=9QcWdAjt_W<$T2!YHiKNUpWzBue`AO?pfE19uUB)J&>`)aycKd2d-tm-MBX ziq1L&3~0hdPHSuu0xd5M_AEB_6vGV!gbSdZG;a9<$T_#phRP^d{q#=j zC|})RtYQV@-EdNH7>4y`qpl=a#F@h2a%!X4sZa@*VP5N-d;-}$wu5GAg6fwm2Vj6^ z;(BIl%yBdSb7nW4{pe-hkKIOf+yGG2>wxa=bdNq-;padz42yDU;A7F|=IV&W2w-z&?ztX;+C5|S)D z>ei`iw5abO1uW{$9~{$u}mAV zs5Q&Aad{N6!KU(yM~*J{J)Z|7(R^wLFr^R3b8h>ji_#{fvryS;WzM3BO&mTc5hRD* zqX?Rsbh5ql7;Gm>2MV742j4utX&y`-CEfjiUV8{;k^uerFYG{QYuVabn8o0V#G`=*Vxo+dcFL9Gl%f(DEd|T$cJZ4sZPO01gQrW8IL*AzFr&k1=Ge{!BM) z4!G#&BfkzFQ1e81jk>G3!O}EsbNkXMq02RyDZOR!w2?{0xZ5K}fZ1jYZD_rd($cPK zlngdmZ=)g|7}_8<`mWz1zqJ#O{4&Ss;K#va;E?{|F}%9^Yn?;64!wcTZo7N%-#|1X zQ%}J>&(+eNoP1x^^>n7Z&GkgA$xt81ZmBz^LG1WKUUq8{;YmwA>`$aV;cdFXmzQS`cvM>ig!?-nvv3 z&2jppwFOyS+r+6Rl62iUe>kJ-++Ye9+a7_myf|_V^XuiZUzM}V@&;Lzy9iyGY|;^i zgN~LTWZY3Q_e&zi!=7NQIS`CXxENJgtn?BFtKC1Y_q9srid9LpObX1=&`@XGxv_I# zxUuRM;ak`l$wG+#(E{w{B7HRGPxR?3dARZDt1wYV$WO>Ty9Mn_p^R3-dwKaLB`(SI zb30D%eAk0^qi-;L0z2P@=Q}Yj%yn_o7~Xh~>HY~{_MyG3A-b1`NEnHB%-+d5BSR5= z26t&nqFw}*<2%463u|+6(=of>i@#)616E|brDglIjj9AqQMm$5z{#vjRVv6OQW|(I zMMTjeo>hm54jw$y^y!V0BI$f{wL z$$J>7HDD`SR*lhejb}1^cW%ogFRYH!1!k*ig$FZJ(+OTUiyxG1BD<JS z31wKO#TURSev3ZGu(rhQ+jDE^C5U#>X}~X6J=>7p5dBe-2M}|;JmCuTX*&=|S%pH$ z>2LLZATSO*oVuvMi7x{4#nMqa3j>xUxcm1cFC45}71;DAO~gFq-t6hCKPP=k#k_Zh z^Y{Em$j;9=qMr{SB$mKIfQ18Y`isG1*TR7g%UhN7N=F@Pexb`D)kQ?iaZQ4SyjKY~ zK+B&s=E#K_9GZRaW~_mjh@+R02l}%m5CKPl5~oMRbPY&RxB(sML?yGonm*jb$bl43 z84~vW#|&kaSQl%XDzhqG2mY!HZ+9TjW8X8ElwQXR&@gw}~VZk3;@- zD`@@?M~>tTz;OZ`mM#&iB#thv00){{9L*(i0BTI^T|{;^%2yD1*1*Vw-CaqGFOvfY zjGI&o#DW)Ho}CL=v#!Dr!deMMmUCBnXn;+S+Lu1x29>y%;C*S`&pEQGCZav)s-8{BNKr7@3$QzJ_!tw^)T-5zJw2h`<|(FNhZrYc%$&(Q z7$>)oiHT)-EY4e7GQRQBe~ah=O{=%l*|oD-{FEibkE3pZX{tSXxRLN)CRouiOKw4d zohsyhBQ?KEd6SwcC_akqSnZHGa%RQTi%P3}HIoceMwPFe9vmRgc4hmNxVLoZDi{X# zE1HyZ^FY|`(Pe~Qcq}@U@@01rQYKp$5CY}uu#rUvPSUfMY1D8xB}VNqsPpRy-GA3} zvUF3%tl1n`mxtqiw`xKb^Eq8#LP>xP?}IE%-+^mG@FizSWwOON_~k;(Sbagf?&T>I^AS9Y*2 zB%e}n*nq{zQ1i?eDK}F41T_C!$q^(j({%>6bnQbrz%_*naN|>~EDZzm_gBht^6_(Z zp$-ZTS;OGWlkFGB(D-#wLEOR66>iPx#~R_oI} z*H&FDHSa=H%-MajVQUL&5Ba=>nI0AR42)ddxDoSLR8&)BPhCHAj(^sT6m$tOmtPaf z$Y#M})ywCmBQJzEbpN}&8j_(c<_hDCLL=_{np+xrZgr@{9SfG}G#L;rdd*%VrA2=6 z2=t)pXD3pyPwNV9#znklanG;cKB}X_WJt3#h(0u%d_uhnXyt~DbUq{*2#J6?*Z(!> zBT<)3mp78F+6CEw?)M0d**2og0;Hj~3k4JOF-$`)9`HQ=ZTLizp$Ak36C(sK6&-?g z&B_!U#7Iqf3M%@98;_x=*A7Ma#72=WkBr?zs|>T!4s;&bK1}`G6#yc55g3u zun_Mj8UBma$DIE<;M}JK^;=J>r2_cFJVArV6+86NfYDtuFwsu?1e9=tH#dNu89Ed( zgF~uCkshYx)vB4_r7GIQ!Y^^Jd^+mC*^P5SNP-Y6V zRFO5$=VJVIVo{~jYK=9ac+jM8Z8&ZKe5X(e;04j5n!DBRe4_4Q@8B>OR3`)GA##r$ zLPvoX3*Tw}$@11*`2-f9`qu&Xq$(C^n`Hl1;2H(i6kX^~w_L7S+(sB?dA8c#%MHJd z2wi@X*D9gTq~jLbRXwnEL>aDx-RNorw!rW0MP`=X)vOx8$*&4?OhUja1!3RL9&Gxv zP71P-;psbOmuY6krh+SbDZPu_o&B9!@8uKvUSA;}h=wb6ALCHbmAN3y0k8WOc78JZ zbh+nz{wshJ{{S_+{wy;e@q6U<#&^k%Ml8aLr7_Uaoat)tR1G7nKj^F03x@FB&=6^* zy=E^i%aKsPVRsioG(&;_vllh3ESz?m89nhwt?Oj=>0vUkuU-vZzxoU}s>G_@PdMmf zW(tM^Lc;kp_GE96;p@+m$4ycECcjVMy%q)Q$sL3xuXY`T$4 z*gdq#?6PAHKumyq{k=$ImKl5ep4{wn9)Tv8&~YTcyPd&m(|H+?1H>E#oyurH2=5_e zui6bQ2R}>T@TLN1@WMr^j4!PfFw37D`t@NAA6i(;DvPP0-`78UVl98W9A{fCY0NKR zmV8}BFX>z8PX0_pJW#S3^XQc88wh!dOWk(GlkRv(lp<$(!cgAqk?dA>7c_aAuXXR| zc{X!467PukHpzlH9~0q4M2Fhn{pd}3DYhr#Y&)XR)+@pl2O`G{k6PAK5c9h3Afc zcs&4zilA}=eh0wx!nn!f4HP(rX~Z%7Qp?$bZWoq~yueHImuw9@4l4Y{hb5F!*u6Z+ zJ1}cSivTAk=Cy#5Uq+z-LYkSJE)oFoe}@Z)pTp{6>D#=GkJTIBnh@Zjqmb^WtPxwx zfhQ{U)Fe$p>w~^R-h%{lbG+8&fE5+xYc4@*IYcBYg^cIZlB~0PnsdL?$-97xU|EoZD53gd2>IQeywco2Zt=Ft8 zjN%+SPxH7V?*bNLNwDypI4@ukSCPsh*lA(_)IzKH%0k*B_;%A`afAqqZTPvi|_46XPzJ4;In!Orb@B zG+C{GTJ|DClTzCo^n!RjccivQEphcd!$`3dK@UC;z8Az4M8%D9x3+Kw zukp5#i-T<}(wRo9r$ce@HCfJvI)?!<#&2_lk#iL@3I z5gnA~rbvmWCzQ{7OfE20*>Ai*LB7S-}}g?Na5Tk9aYh6F5uiItc##m zA`8ANo_`yF;xHRCth-CU)H+=|&C=pIdTlcXW=vO7DU|dmMm554GVlA zyl6X3g}XTAr2C3d)luC2q`j;Oi@TB7_lo9k-iYLV_%UKR&SrjLf8)OO#-r+*g8bmq zUi>767{|(PYN#?Dw68F&aCyB3U-TRPnpYbkrGwk0D)(z&FYV1-+FhZa=OKLAk@Bq1 zy!Dy>nwQ^`Dcl@)fN`>>oNjkvVG7*Gu{!zB#&dtXtQ}j*VkgG~m0JmbHGD^aZIz`( zahs2-fkQ+)j>Fcy@4SJmg!O`Kl=vXw%zC9#DlZY7TyInau=oD#?@g>T%EOXp+8l{F zn57vbBW~jbK_ig|myOE!>$atrdd`4BsonDBu9Dy6FppJ=HIy|#D5Lx5pT4XDR%-lb zl3_h*U|FspzXF@r;X_~I89(ULX-q=-J{7I0l~--p zs5r*3GMZzs)vvUME?;7rs{X~y<^MW1?@Hpj-)svFBIO6|HIIN66^A_hG};=oS6qVb z*5<}TyrfQi*7(-=B9`-51uO6Q(1lryNmHaI5@!Buk64A7Duu9Hd$}Kp}=4 z+&=m+9nu5#wO4A%C}F8Shsy6f725l6PW>f=@cLHM((OY*htn`xGzi*#=|XH$lsh19 z{0&O_QDM7;K(5{LicDCUwrZlYl$J9r_emY={_;h6@v}3k%Uz6X(y(guO+v*ef(VwlqGiC+6bE%;E z^(eOVv=F+4Ru~v;!+NRI1w@_7+@~V|RXS5>{TraPU?EGZla@8JFLXh?i@qrs7NwF7 zpyQL7w<2Aq1)X&Q{-FSLSZJpPZ$m@2@`Uvg&f6iQycooId<|g39h}kJp?Xog^k2<2 zYV~ndxKKBG_{&hx;GW1gBh(c+k8Qp$Kfny)iHF2B&eoAbO7@mhjOLb_vC&fw**C+w z9t*i+k*$OTFK-O$8f~!vXDkh;i{jw}ShnHCk8hg;h%u6OoH9|_{#%;xp0gSCBOj(v zK9*$Qqst4X27f>W^Do48eM5UO`@F_Y{@`Fqjf4w12LrtnEAMibF|)E&Fl3N>u5t6q zg81{J24cQs8z8^&F;U1!C}i>8-@;Lq2{f_GcoRd}n(P*ls`|J82(oE9IgUI~8OSZW zkU``=k>`Gpn8cxR>kgWAQq}$XfN;Ot-fbk2AXk~8B#-OP$)R$%MiIV;RHL?(GTh|8 z-F;#7`LnLW<6+>)Alm;R5ki%;Ea*&Zd*_D;gtJxeuIIwZL6zw;FnWIJUZA4uKZWs@ z14cNKNn$^bso=+E*>z`tK0Wf_tQa8&Jr)LBMX1AWI&#_&-Sg9Z|F}>$vT*tGajX$j z)RlioMw-%|DJ!)3&M^@dkr%lWJnH-G3?r>9U?HkQM;*ovTKXT+h2<{*W>w!ah}=`@6ZA1g z!zyb4Suw+(2OJRr=q8^wC4gR0<38}YyWz$7JSCX^``2F<%J)@<&%WX#442&0YaTuu zMF80ouxP)I2T=#(lo5FO?tV;v(uP>lF>B>Ht$rt0vW-)GU8N_Mq~YFv1naB6w-*L) zm4hD=p{`kfNKzAIcoc=YG5tf9n=e1NYT z*ZqtlA9Dx9(*#|;=@hRq%SM)%s|*j}vPqa_vR!B< z3XkuQ=|z#Coc~XPi7AK5rH${KAje0CseHQsCVx7K0aomtRmC?g2{_9E0RGkP+#z^R z!8Bjif&eEEfb;IUX<-MjXV=xJ2^aFFXHke?&xQx(dG&9*2Rf9vfCZ3#0P{5 z0Nm+1kN4~Kn8G!hq-nw?0rF8dQbO`enxd@pIk)l!`SQiUbOg9fBZ;LLzfta%LKImL zoY)$49PqN^gu>d)pxhU7&nt2y;c?%jDddo2U;O}CadSpyM(P)1yQ`oX;L~>x_$=v) zrWAD56k`C;9?v?~aMt62_7#O+z z9i!)#YkvTkbY?&>ZwYOB`Umq;c8za32^V%OGyCs)GW=_O=m>ysRll+ouz}a?A|0Zu z!6z7d7sE2BUnl=re9iR)Py^~!BUG8(gvwk>nm}wdb@`k0=eo*2SNgL&=Fby`+1ITF zKI=h_yFrM)^H}j)dCqM<*Ak=KZREFzW+!1n&^H$E1|-dI6V-DSK*CBNNVO&ggbR|P zGkvmwUr2<*Rr1J14w1W8Gmdsj72}L1jkfsj?8y2WqjUFBaIoHGdsv^If}IXOQ{P0d z&BI~?WOMLz%aZC#t$oU?05bG9+AUTw&wzjrty5#P2bT4II7gdWvnv!Q5moMw%IZ$*nAiVH_Hvtgg9HH5PX9I8_O3N(N#NJnpC3ppKJ_5)&`Y|YA)qsY3Q--(ngf&WcjNuhmr>#bD z1F|Wcu`yuRfpP};2ms$*xyBP7_bSf;TY~2)R=Fx1XHU*%f<3P}kfVG7I9jHMy!gjf z$Z@qMirx8cit4VpYHj1NU=C%{lqp?bDKlSSa)p4|PQP;UheP4wsS$N5pjcW09urS< zzM9%n9V`diClnPJDitzSxmd?KEJ_3QMg`#Ho0dNN+7VhXkZXE+=CfeB0GR*2l_-oy zR90iQnW&t!OOmS4_$bpIKdEA;D@P1d7iqu;pk{*}FLD0kwMS^!gSTKM;ea8SZ&`cg zhpJ(_;k}+Eg0#z{FF|DWk9lm;s0wEgVd5Z{`+i_LWWrQ4!ByC**wTU|e!;q$fAMPl z5Zz*qIG{>3S)NJv0p3+Mr=5|=&G_`6?%iiIy8OxYd21(c8~!iXbMij42e}?KXXd$% z2MqhmC=4?+DEUTYhvI@`qlJ6~s)Ul%*l8&(oLF`=01i~^EYgx1t`KT24Pr~EPN^=63fa=$K0}VkXq?9+VPSdqT&brlJMK|q z8Q4r8XY{`@t}H_)&jnWlK-2hz>I9>hLvO^~?hC!DW2E7R*z6k-9-fWvV|*ln41?rzm3B^>>?^4%i*LQ z9O)2@mi5-k^$ej~ROFZNH%5iWe@VA&IbuWSo-)wAmlBBl(CJJUA}VB|wFuq1kz3rH z3P>r1X;!2}(qdrhk&A#Or#cxIJuJVwM$ge(fNJ^r;x&ChIH!tg`8XiShNHbPvQ}=aN_NnR}ik- z15B>mM93etrS-v20{w`MxY_C?l+0TvO&>$u9CyT{7H2kdC~xaB3rjZ&t$S z0LLZvMv~2n2lv=f`9pp-Vx4B+i z+5#BWgIz%)=Rs1+6k%Yb_ZKVnKdB~&%8qyAzV6~>$&7Gry>$LrbAqu3adwE0s;^*m zbDppiB&2zp%sjlcRdsn#yDUYAr%s60Gd{r40zoy}Vg70?5)P*~`Czgzh6n>K$^H6= zds|bf244WK>XDQ8hNFP+DrWha?F^-v`W)}itW`JNH%(Sx$ z*MBL`T-VmYd%<_WF^*DQRfCec#qTRu5e}lPK_RU;Zz|)k4PGzeKnu*9*dOfC* z3>U{LGXHbx_AxVus~1Atgi2hhYCtg6A|<;oK41`OJ*1@NwARh;egEn`)*RLxVw16t zR{W>f#u^}8L;K-Be~L+z^V)(lp_Z};P0_Nt_8eT@?1fyGU1K1)>XjY*A!!M?FHe!V zHbp83FaH)tAdQ39;fKMRzz(IXd_AN!kG>0q4IW1BsXmRq-y4LowH zMg|Y*+)l0kXno@OV)?s>$KT0zh=5>vnBOP0s&SjiQSKggb2|0NzIT4!J!B!r^kZwG zodBk~^Bff2^@^E5wW2Gk2u_iRz5!8p9Oz!fqD+IaP?>5 znPc4(V^Pl~Xs1M!QeJfi)3;pokx!$b|JvTkn=7pqL!L8?)kOf>>tTN~crI^PBkWnw2Rz zbQx8tjIc(w-u!RLx6b`ic08Oaost|QOng>ni)-e8MzN4}GxqK~F06^?%-ZzpTCcK>jvM>K307{eC?E z(=;3xH`Z)*aw)X^G`KfAl9fZN_cxVW?8^=Or{SpifPn6pQXV=sNwy!fQ)y)5D3JL$#%Ln5!lfF|ksa@1#BXc>iM;;lJIE*4+DM)2dWF znDA-I*r|ba-Vq{(SYN?K0$Ps`%z~+hkGX3`dQQrdQ@&_X{P~*K!Rey;0!c6qEwqs} z>T*}LvU+I_A}{MkW!NRV^oDF1%6kM{Oy%aeA>hEoYgNML@kq9DBdzBI3!G2x_;|ie zZZ`-9tOi3Ym%!DB=b)phZyHzJi!m!-ohcmHQ8^6S_J!K`^O}CG8uDpKtPLBINO6oA z)QtKhca3IY<#-2R=9=ef#)j|*54mx*ltD``iE;8u5!JA(ZJ24U>jto_OigPGqkSyQ zuv=)PT9R?0n;&5*uCm5zL%yd|RpapO)~1(ML92!C1Y>LSFI7HY(e3W49m^0T9~v71Ca|zZjKk6Fdfk1(2%`9OMDk{95~Szl7rF!pNnI8zUNE@>knkR{p=sWGw*8Vd3336kB&8a)}vues~+XxIN>rn|JJNVf!_0Ucr@ z{eWurGm1pJ5eQ7m!@y|KVE>W!^5ly3!&V4wg@jW{?^B#uE|2f_S51Zv8s{gaJ!x7U zKwDp%d7GcGDs4|e*WtTfMclrPIajjUsRs^#w?sV084S|z5{wxCmQ`8eIK?8u%w%u}}7 zX3+qKMI18Mg=>5RhaX&H$oT(=x(bFUyRAzjNQ1Prz#s@imxLf40xBWhFf`?|Z+00B6o~_S$Q$y*B3q&g6|jm{Yv)PBle-ayJbj z>ouaUtUKQ+(I2S#TtIFvnebnpl&?TKfZ3-bRu2|>kLWg?Pq z%JtY}3f`&BMZIqcwg_Qu6mMr!4f*~MFsqZTLeqQixR^J|BcQ* z*I!&(U$qet^8_sI|Lto}x>imHe*gB;_Yg?hAW7=B>5KKDWt`^_wk~&(mHPY3P-HD& z!W^jkYbnn6b^l&lf0m!j0K^I{ZN)~5NQ)eDqM*2uaaOa8AxIXd z$4pyK_Db~ERAK=QkgpK)#Q-pD?mILrz84KR%tC{%t`}P>vGWJJKh(|l`MU$+0=z8vRqG|3!F-amS_1YMXz}#>k zh3R7OnqK9Y_rJwp2Ge(z@$_@+09pM_%7|@QeWG?-Q}EiahEeKnK4OE?6xA}KQa_g_ zfl$*fAivT&TyhG&CBi$Ju8gjw7cW-xUIG!p1<;dZsvScII{;RbdUE}>GHl8Ds5&hP z=y2qK3;jm#Si)FBrNpn-dM)B(+hbry;CB1yOwvbmVC$S?)mN@x@z-6=K%_XZn&ZcX z7qwziegq)8XKP$HrxSe+0z9AX^CI`oW!Ltw<7TBqE9~>~u3y(Pc}eC#e5TVy6Yw2K zOI-@#%KGdH`=#1;So0#|o^_4_hcHz5p!_`$of_9?QZA&Q;_4#$d4BJo^_Fm{f2|Aa z&=*jR_POErP8LpouDRo~&;VR8`r+G}*oVy&w4>+Nua$tXL$66-m~*92y>`(;tPG%o z9&A2P9~a)a`Bgz-E2*Dh4%*CD8DjZ9YieW_=dDsAyq>^L2c#m&v89iDqU2X43&AhF+sK zn?>{tfTkaZ`sg9!S?bZlQ1>v?q?Xm)b08B{fcMVPXK&T{nHD#I0-FBN3Y^i-q(x$m zUT-+v4O|I8hf{&h3`mum;b=e+{LAZsZhGWTr*YqSmdf$_|Ft+42iCC}M_WCYqi9iv zo7AE4b=)#&;sE(7fZWEI-0ZuQ1w?A1+t30-a$i%-Yq;qOzfp;?yINXkoLr3o4>r&? z@TQC&e?Q0Zcg3LK#fVYBsyNFq^mK0;v_$(4B!ERrLI^+9_PYM?*lC(xOjnVR3AZ>m zFsgVn_6omeP_G%7+W%!UEUBE400kB?048Jr{U3ND(mgQ%;Wg0XkQi_*KLw_xg3b6_ zu6QdhrLS&_>H$i!GO(*$g7~x!#gwhGwo3O`5;z4e;29*vk?oM4h#8@x3zoQh%x0&+ zulDN?QIoGTEHHQIzorAget91Cmv1c>o42aTJ9c>B&7su+NN??w^uzu#JC#2+;-teD zLa@nHZcZG*`n0;fY25ELdf*9=DX6inzf=TY%AxhoVSa#tVRIaV9cjK)v$?2)pcf~I zD7OOhxHsMlHM#FKt?*Orky$=R7vAz~J);a%3H-8a0R1+8g$puL^VmJ@6Rt(mv z0h_qCWkZS2y{LiY%C%35zP?!(k2W}&V!M@dJ&e7ju7cog{im$M%{`*3Yuv*P{ThgQ za`gTN)X_-W==AiHQW~wlxZEygU6!=34&IY^XupBLIG|_y7OX+%Zmi59_f}F zaex`x;H1|-h_1UW9b|+ql0X*%OM7k=Gj9a1Wq?7df1IE>R7v5$;bfl8Hu+t#oSp(1 zbENkx4)ph@$d3mm(KBwP0QGfSJH$g+nlH|ABvdNC4SB8~R!_T6Z?ll%tAD?q#>;l{ zpVmnW*z;-@#0V@|7*eDL>|_D_l@hTpoa9e|+_DM4GZH7gWEu6;4i+eOO3PbRyOjG8 zd!=*IIzK8Rziixt+ypbCFV44lzF&XJcCVIebFP4(Q~+TCA)Ny(RmZ_PXd!XQqu#GbUNTbYJPr>0C#qIm}B;VwQEfdD`y;w-&25Z-CMsy=Xjk3FOf zp|DyT1ZN-Vr%Jgt-aqZalxSf+N%@-=fcn&Uq``~Sg#P3tjr#AwHEeVOgD$SCR z2!CAPsF$z*%K(U@En>l-{~A&s@C*Z(FQpFSEdQB5|EKQ(8lwiCE|(VukeN3h&{dw_ zKIx20jf;7BP;L1Qu(@uy#f(P@!ZC#hp5=4g2r6LPF|_pCN*mP)2`@Uf&<8TV3_6zB zYOSrTbvp*IFb1}h9lVGr{)Dr{UP%rVsJ|KL;Ay!8hhJof_;eksA| zuNcKUw64i?c})roqm>$P5Bu0!6ltz^2v@?%Z|UUbpY%gL0&cs#k6Lra^ts$^u?b zZGdw-R1{HEd5Qr6((xO}U8L>6X+V^pqrrRMYwm$sfQYY~0o7JTOu=x2B>%!|8gT`kTy1-0ID@2;}K^*4l5)6iixL<7k^;$RBFcs*z)txIY*E zjAk@{SP}dQi3JJ;czCW{C8RH2RfLok@1pUHqpBfAFhapjA(S0M8Lt_zQ#Qis=iR9l zA09KG0&Bs>$w?GE+kd6~CPFol@BsB8ZR_D_C0IfTe3DxwlFGng(G~*4Cp5U)_8n(YJZNTb_H0@e=d1%m)l?{2Rw-*d;rt*&^w| zp6g*XwU|wiGDY944u2Olu{077@S1`?FCJQR;8q!V(%En?f<;O`jm8igo+XHc&g$T= zQs2k`%U$&1zWFVQHJ=TJsp9_PbH(rXDsqyucmaA@K|Ar~L-I_0>LoOq# zJrz^wsa}-I&E8&db7MmUk(|SCKl-EuTTSxIF#GzETg#ZZcN~AL+)5!SO19DH5-@?3 z4Kn^^5iWJSAGJ($3RTb?fffS49`E{YIq)Dctzvo=i+#iJWbMNgE9^*%Q`>{>oTN`N zE0q~c1SKKN5TC3>#{Bm|U)EQh)!KW8c`I~iEAy&EmhMh{X_P?6lEgvZyVP459i?0| z0?Aj!2~89~=1!>IB>FmoB?Oe4DWs?pQIH)NgvG?eCu^40;qdqA5r5vTZ}nQo<4xVe zz>MTVG4_E0>dKU6=YcC@9I~(ff4o2YI(Ky0SIj-2IZViS8G4TV-ws|Ml%lcspAGlh z0?V;>-3Qq{HV9ZSDkDT4MfA_uIkBZWoF_mVRW5_`GI)l-QVHAq^b$zJh5@^J5YL|B zRAyNk9d7<*sz}{b8N^Z6%9d(zK?zKAJLv;RB+t3kVp?9~@?^16mIBC(a z@hLc8tyQ*rwhW>=v;3k{8Or$ImM5@*gJqM%M^`y-T*+6NS-UO>xB0p*Gp<_3Hn&(3 z*<}(LILdKKS42CUbL^D1=FzBWGxQsWe8LOFqH z0w<_g@?#gWOXK9Le}cYv*Q96mwGaN-DT!W4QnO}fI0QXrLm1P-+m>VgM-cn(7P)8tUi)4k{m zU<=xvsf)#lW(BySVlByAd&0@&*c%d8>D7@Lxw9&tp!roS!0DKL1g^18r*6)poK?Aq zvgP5HwWRVh1{lRC_@?!pQ(nDB5z87kMJTCw#Y$Q{h_QYX=RVfXUF(1k;4r-4z^>H=k?7UuVfN3*7f(> z&{J-}MPpCiHRJ%7Zr3W&hRUb~W4RO_i=u^=D&zBG0$+_okK( zc(sQ^o1WD>+DC{KaIu;;zq97R#J~B3=kk7fEhndd0!Jf<2}^ru{9>>;-V=^fHB*hA z1cb*E%#A7$y(%7_^ns;vxyNY6GS`R0@;htLeTosB+4LOBK@GOC7(k9`j2$lgtqp`i2 z*KqtHFrYD*u52u*$~s2}J52F2Cz1CKzWj=Zm=O$1vq&)l$jRQBbef z-uaTYrFD9Fo{2`bK;S(gZu2IFne>735Ee=EFnSodi-ly2D|;mG5J<|2ezk*MMyBUn zv}77et-)M)Iz{qAiVDH0OjeKt2+Et&{QE0diZF4HMYs{ss{=@Y`+2eV!S;31(?CSPlsH`RZQhrzg+IW_HfxqLdAiJw*5M21mFVP>n>MBu$7J#5J%Tq5AHQ&S$? zRUl}+Jx4Dlg!LO1-Gl&zB-lRDuX&UsINF>ft*;iezJTkeV~Amga3yU|NvXkon;7 zL7?p)j)_0?%9X3dGq$y@fxW)M54-h>S1r zd-2U4&o_zlZlV2)`<90K(7ar)Q^}oO1N|el2N`KF`U1nF7flKd6n3}DC@!f@1avRv zl4k!VC++VvBdaEZ&gk$KTL)PsUA9`2@YSrgbU}Q{_HRzYZ;T5r5 zaK~M4HU*P1SyIy1B-&P~uT|4xV;V#*^J^W_>T8*FeR5JU8JX7JJ?t|cTZNX6ugqHA zUH8?3zR<4~jIR~$(I;2qzlebF_!6;a_9#Tuk9rG5nTJpS*KG!lrD<rzVfw4XhRDUQsGuqBCOZ-er4gb{GBgACvePw#j;H}mfJ({4I{u;rR~!{&FDB{gAL zce%I=(xd$!MPuz6D(04bhpv@{>jIPQ?>a?MII0ibsq+>4GXnK>eihy9?uh9QLG#Ma zmc@=9^#}cX*f$SSKQbQ#?mcE$dS~Y6Mxd#ZCrYz>8JOgo`BeD3EwS_;U`Yl5v?mYU z#BFV?37QrqEvDbb%i($k-Vjqacu6Gk-@?BYfsqnj7q%&d%wE zX3}>>_3k~DZ-R+(k%76-j6U)ea`Ay>Q}>q3snf7INyx*Kd4NzcwHFvM4c(Nmd;=cVKB3Z|C$pV3PC4$YwtkzV zS90uEKwzLC`v2fcD<#ST6Q0VkYHkIM<@*yKQFp27#B=SV)JY{O;vCI)_)&FP>RS35!1jggQ8;#%IjZm~wnfh>WH`3AG6 zn0T*0{P!J~p6`<$JwOd7I-hO?zxkF-=9oxqb)R{1Qi;PYynliFS2AA#gK z2r=#fl=BImUk|3lVE?wVwOdedDJ^juNb=x=Rlyki3C&u+Sd@-Ieepz-dOq~KM%A>q z`^y!_iqMd-Ut(f1RzNBzQzUtw=qTt>mk^pm!p|e(rj|By)Gi2x8iv1fX1jai0*qsy z4nL|iRH)Fgm7hO~Ys`&Vx~t{8@s$JiruJ#_i3miF`^cMVx_N%_ zKukSa4d%?7Z?W{f_;isFw`*;7nfl<6!@|#XUHxs!4k#4;7)Aj^iksY6Aq;)-$8@mz zkzKL1)8fVZkJSEGKks%JXWrYNLUeL1{T)9X+i!Ee2gP-Rg*B4RXa0?Q~ z<+&%sNTQ!-QgJWhb7iObf$vwd z?_0b3MOar;i*h{mVA@KGSHc-qTn!3$P_Ok@@7mCgF1IcC&44OD2Q@$V8^*K<^70;9Ppj!0)U{w z3wKP%J6}GD4B{HfSf}wHO#TmekyblK@%FD;I$C{TLN31s#|n%!by#EaxuJvgDF+*C5L$Opz&1^mNXFmz*C4w91o`1s#lWFMU%&eJ~F9BpL_&CK4+ z{MM{)6O5jrCFLj?c|!9%Q8GhzP4ewcmm)~E7CJa{xNdur8@j&Q@_6&%aHW8dSnO*u zsxzBv<&Lnxo-GpQ?4{E2Hp`>6kc*AqQTLFX3Sna7*d7op z4;x$e2NC-G7UI==WWl*@=9pld2Y_;d)6dze^@=1|N!>y9vZ3(914V|u3Agz7Y2t}k zA2h$7ai$vl@WbpauV?i-Qz`778lm957Lb&B?&ma#ZpoqtaH4(jU}X?mk29-eQif@@>q2cg9@ zKktwogGj9N^T%EI9Jrz_Uyond;qW|hrC5y=+u$4SnB3KY2w$DHKL#G>YmKPDpyQ66 zMoK9-gm<<0HHcIZe9PIr<8;ZP_};4w2yZ;LwTP=U@t;+{6X3iHG~hiAot{n4%w~Cj zp%l3I+g;{b@d1vOJg{Bjtuqh`IpBLvnjK?{;$_>sIoVq*d&cvsBx&;E>VQ_ZrKmVj zC;d+r;Jh+U(vft^qHz2FcZG%hZ$>3IN^t+_!%?K>xBWrXV~xP@R8|mu=$nG8o#;Di zccD1g77MvWmnsf@9i%K0eA8w1;%Q-%2F?%t%5M}ZV)kAjq<#~=_tNuWd7`S9*(CxW zj_5E}NwDrB`hSn?9p(`7wFwLhvTJ$cm$Rw5CNKAk>7HDfV>CsS;-ma_AE!_jjrW@- zhFmHf=Wf#y?HY(x~g-5zbPq{FPEfhbaEKlds{lhyBA zt1pBm{Gb~=GjAy$sFtKE#fAk6`(N@~Y0~6JCWaW4fA1e4BeF}adF%v$~{#`{VT?H_7ufU>(gb!)ioPq%nSB@Kc3Z=Tj$ zlp`;fsS(>NdRDBE58p3@YN;49m3aijy6jp9eYkL6sfd5uao5&kSN{0&bNvh7jH@Cu zj&I{*GDKC%FrV5!1JORc`j|hG8<&dirKs`qzB8Nt$R{-NY{I}Zd}?5olRZgp;D!VM z)a#A9iq7M;+7Y-qYmjJE&X?QOJ~nSXoaW&Mk7}DCIlMuG*8l*|Ne8cuzX`tfiDE)3 zIsom7hISC75y|FSXMyf*z+7l41SjrvWS3!C|M;x<42Spa&es@S(igF5+>IWw1tdyO zCNY$L9$j2b0@q%2fwhHo0d*;Jy7(~H+TUaR(L%7OkmoSM|9IrD(g$b37XU)PXA^H^ z5fX;!4z?)b-hVK9`mU>Fp+Z?hn;nWZ{qd*X`=*t{cc3lTFN@|NP^<*0X|rl-XGZDx z{%>ZE+&*3{0I3^yJA_)5+cUo z$NiK7d6Qq@954(34PM5dImevpgxB$0(-U0jnWa}IoRtcrhaa5p=`2MH$rkbUvq1%x zb(UQ|SBws4WYQwBigLlH=A&pf7!LQFr!nfUn=AXF?*n?b-I;Ka=HI310)uspTjEMm zDuqyy@Rz8prHGzM1MS})C-&l+yn!g4wXJF`y=3SnrG234^q5_{xnny_31>L|P zUnu&$FFVZq%DPqEq8J5Zot{fC;Un-|yaPrvL}I(aq+yAF8MoGk0RE5iyP+ z&JUoxC$b*BYe>PvzKe>O^h|u?oW&;yg6W&PPBuu=3H5}aLB8vdtx{WOFl1^8L#m(9 z8VY7`XM(x5Kk*TK(l$ouCJwxJPV?nG4v{GkTovk{cg{gL5!hm@k)P}h(O%+=GZwIC zur&I+-)3=RD1Mk-u&8jHJ2bLn#}#cWtA578iTxq7yT}deBHN8KvbWYcQZM?1LV?bD zAp?HccQB0p4=j%sn1QpATf9*d^XsQUIs1k=sYoQbxs`ZcM+G+Ca@>?Ww|2qpLwT5w zEOPUXY~bQ3chm(orE~)3oDXqTDL$tO<=L1k2LW3wOwO6-j1OmM5QrIL8=%`SrqIUU z=-pZ1_K43KrPfydZb~w4+b}O7x$b|R7NF3FdZE3cI_xX};K>;V4B4!u^$zMB_nha5 zj&uFre}8*E`xUjdGp776I1iK_*Tuul7SH`zo22r^eX1Vs#Sg5*Y0sTg6N}-5L;d{H3SvL|$gQLneB=9O6zLvad4HC` z;$P)Lk+I2uMFq$4VA)uoaKdBqN^6=vpIQ}?GJeFyR=sM^JscO*_CbfnXCy;&k;rKl4C~4jJ z99Cq&^BHAyy1_$r z)Ov8#q0FXMD>d5T(HFXS{al>;WEM+DAwL|WRY5;Xc2`VE0C0{x?k5LseG<(9ipM#} zqzNS~ptBjv{6wBsF?F`G$0Jw?By0X<^1wY{+!S&VBwaTT^!^+cH>dTQHGBG{|QIijX~@EU?rhM5=q^1YQo`b@P(b%xMCpo(uB+vLmub)X4|*2R%=&nuQ=U za?Ap(@wY)6qU@TZyAhE=0=?ux2&fVWASdSzX8FN5zat{@hj9QRc3WH2o?&}e$j!Zz zJ(AqcH|K?NpW$%Ta%a<7J<$2@9@lw(?=kJ)?!m>$)UZVru_!C2Yong1DEll+p$5*q zE(KiO_Q^hXeruvXf&^IX=y3DG&FT~i5TlQvR}`q8+f69`<~1kCPLOv#!0pGZ1mpPl zJoR1qJh-Fk1h0s_KdZGPw0DQ@j3ZPg3X-lw!6BI82(ac*=>YjLOT}x{#!V)G0`i~V z%SUA72wN=ZBu9_HLUrxv{>{p9qiu$))(zEv&YRPPJig|)hkYAc(JO6|0m|eSC_?`a z5y&4fx4kL}2?>xXUfvdzv*^0r!G^dI96(F*|DIM+?oyEdT*#;R8}en$pt9QsoCJgWayDtL*&H#6G(F1=*2+hmI zZ9GKgrlrAhK(5bb0_?zlMU`P(X#VG&NfiLePbA8?d0v%=%=JnoV#Fg1BBuPR77ttl zj1htJm!IVP^=(nn|5M<8>f)_d98H#c=|9daG61a?V!Py|dU&lTvcigspHtp~clI*i zqOB44r~K4BhKcXeAYZcY=WCcD9R2ZQZ(E$V_!ucI%yB+8>C~7`+BPThrhW%x0GoaQ z%?}HKM-sZ!etwh_{x)j6pxfy-OEo&()@8ePUTh3t4M^h?r+XjynQUHLTUv9tC1My-#cMa?wVoTwIhN>dYz{AK&R1ao z{Ndd$C^}E2nL_(nxVK7d8=ey-pSDT}1uNdf%kn`+hdYrw$Pu2uKW-vw3V8%DO0Lcx zc%2j&DYlvUs;qW=da+_|-nLhBp!3bwxDVx$ z`(Qo8{sn$ZyA;(KWvgb+2}9g3EBR{^Hbw~z9_+8c7o{4@8!EhX_5TP-;`=aa|{PTk<*FIR68M9D>&vCL>; zB09PbZ1Y0s8ock8Cdrdtcw9i{VPs=ml$*)r+ z>tpp)Fn+HYBwEd;B~L$)H2lPv2VUzN@IrNm1DBd}SgUfdn5#4fmRwD_Av(Cs2|vhW zv#Ng}xy@W)T5n?3sFT0HeD~)%DxyatJKP6QF%=2XK#vRiSFQ{SZY5Mz-f2t}V1<$- zBs)cBANG|Cpoh5yP5J~=mAb#?=qJ%8AqGVi&T+c=8 z|EGjzOebU46-VR?F7VH?&s4dx9{Z>AjoTa5)cf*9{cSy%2FGuqllz&izp*ar&#nxw zpJN+LCiGmEaSt3&Rs)3QmG5{*1M{|bZ#Dp#E6YDPG<3cWojkEud9)Mr8M6W!*JH9N zI-Y=w!SJjMjsJg6kUhZntypf6BL1YzYO84fnc3CO_r$v&;bt-*0zmuzy1dExR4nMA zdQsFO8LV4XAh^@^cqQD$S~0Baq9h4`<8K&DvX}p23B1XcM=gT%_o9p_K;|wY#Hl^u?GsCwBQ2cx&YtMQE zK!sb|+yS@08y#Vw!4%fi*Wk)$(FF;`Ec}4I5-@yNNYQcrVB!V|ye!hT^j0jeuan!z zjgP)7qFlEZ4E;aQe+bq9AGW*C+fC_1zg*PRax;04v`biEr`Uy^G1N7CLJynFw`cl! zr@abjz_@zGKUbl>FCT8yv2yjJ`PSyu4XVy~2XWLn_TKRRtcC*6+SSifPB1z}}2erF$(;#iMChmDWa6E3z#FQ#HX z_!W`X?g%|sM+*ninmwJQzWwPh&aW}|09pAw4w^fVK7N;) z$lqCaBkV4qW2;$Q7VNit7wW=5GvvP0rED(d=hLlAEWI^n_^2=NUwt`{N`A;~!udEu zM6sT}=)Ha>E9~m;3+e$pSNct8lpDyUPgSm#)P|gNu4oMC>c5F-Q2(vatGX)h`{gzw z1k)c96xM>I@M1Rnt$!CM99>wL^S(r6u?#0MCVBV)UhwzT-?lm19WI2-sjv8`3 z*?-i0O*JPemIu0SCgioSgD%)1N9VY{faP7WQkL}DgqKC+QsRo)hIAyW$Q_Kct@v*{ zST>N~Mll6o<2wp+yo}l*mk%NFY{EtlLlGc`-c1LS?8!kHqMszsHR#lh8Qy(1#QWa; zS(+)<0!Rd7WIR00ZEf{>agAGig&Nwh0d#b(j`2U&;{xO>ZQDp*L9p2_%VJZ>R(AY0E-}LTjC5e&nMNjQl9#fRjdo1 zCmFZVryeS}K>cF%71!1K1*1`ZHNYbxX(3jUCwUbk$T9{vZeHbRQ|E&XbV@?X2)Tf6 zzWFSoOa2AVF2Ae8c!^k^3T(=oD9Qj*?(W5#%9-l-wxkB?+6|}#K{+aywVOs8KB=J2 z7a#&;QsyO5Iz2Rpb4MU^GFDp|5ZxBlZT`@P|kWz`ZT?l>h=2bc- z(QJ2*EzFq!pbCl8)^K6@@0^QZG3aBXdSCb_UYktOwly9DtxYM<-@iocDA9YvRcB8$ z@yr7NV&7{H&Titt3Zb7q%2H^R3PJ0dhg-Ab437hc`Qo>DoIjX&Bkv)0lFItomULxa zt__?yvDeLGdOL3gSO%O60POL90d{pApK2k!_-W6Vb;c`&Te|~EiD$eLzkfeIxt3sb zSu>(c{NE`%VfrT{3v`Pobe1yHIUe{sm@8K7Kp(Z|N53~$cK8TrJw-zf>Q-F1qU{dV zJ3wd$z|l=SsmcevcT%eFnPK-HlJjM#Rfk)@Zau4S62@_;wrBtPRyFOX;@bN!SC5Fs z`h)wlV%{d;`eYQ>Kg`I>UBs>~HTP_#YGW$a%gT0Dh;q%^pYJGIU8I}eV^C3{tibT} zq58wm=?Ca)$eR8Z`eH+T!-O6}kPL%*fenmo*~dHQh3}pLZTyEug=J5%dm7e?5`@Kg z=y;+S+%6IM*WC#fwwiWDp&DIFT~4a()^i}u7#OyGI-3)Ym;Jh6+@ZSN9YE2e2dmby z*EU&Wyn!DFBdhB`$qKjIX(6nWhQ0+dc)CHenU3c~vCuK8AwUZ33vEox%vRAc`TgS{ zH!C_2gxvvt01|uNx#mSA^&U9ROWuDCZJrN1f}MhOrwDSidtHBLdA+O%ux6WyrH|<8 zD%QpWE7Z=NLj^_0V(?skmQ}M4fED6?T|h<9!m9m(qP-vb`K{Wt>sqtsZ)~&dy39D# zlnsDz*{4IL82RHlqars$w5j<7B>IL6X@p~QcerpPlB`OhLr?FxF(MM@c72$27rwPK zAB`^v#SImHqUvRfGvG-7f&MJg+YS2NU+63*?xitdkj*R$O*U(}(6)BhL-VKs_~biH z+$yrYyn*#AY;u1%YUW0k{rzDl0pJOQ@Gb*Cz%DUP;D_^EXA6}AX})_&OgIS*Ua|MeUy90F8*S*SWVeRDO2pPU_hF`*VrW)Ra|g zE4v(^uqY=Wx{*gkSL>!ETBVJbw{T)neqHQ?&VTg+?)$3NF^Bl(*{2OnP$>*Sl*C`X zGfwqTB3w&ytMk(&50N_cfGcDf$*%?Me|+>UP0)9aPP~oPtq@LByAm$GqYW}Ge7CWA zXa{t^3V_z$YaWxYe%)^x!yv?VVvb1wL|zh`N%|G)sc<>Wjx{A{0R?SUe9`RHQH_+C zYN58L{(>04eOddtHbBho0e-ho^yuYJ@Y32v5uBu0yedHA2Uo1o!N52rY%Bu;TgIRn znivp=^KqS`7=a#5(v|G>);SCv+c1JCaK?Wxk6PeP*DEm)kh51dRu=dJ=Ak58C0C8R z8?9+TWIuxzy-ul+cUwCm3I$O8l@WFU>v!(}JMTK(=BNcgV~fx+si=_rJUDCqDl$Ls zJLD^w_m2UM{mtF{!Xo#GxW1usR$`Ua^O_e7^m+$NK~& zU;Dah4hGc<9?YR>(If0hgtFQed&vru_I^@-?6PTKpI5U_r|3u}5Cu#l01#F|&p{N1 zPMx~YRgX=i)mt}wnUCB9dJl0inap~ZglZK$D?wpb@=M96$jsbyHcE+z$c+$f(AM;c zq_0h}pjCid(1#lO^Gv^*n(kijX=RQLHm{vtm-Y=&S^N*hk|9CKvY{Ny3kRB=Iu>2U zx*ms_b}Ap@wDHYkzkLy0Kb(E>;DdUm5>PWFg+r3Kz2rRk^&W=!R$3b0Y}%qa+b?N= zg(t|QaM=eAziBYNF>2D=Bep6I_|v`F$>J zk5+)9=fHzmXSUy3tPJz*Ig5c#`>sUv(sE^e831Amk%4rprPnfVb|nOAYz! z59oj%hR$krbvKXxF!-lYS90&^l)Fo;HZR(C47$IGz+2GiD>bC+XeZ?Coy3D(fM}ra#!oZpHsQs+|o6V4p z?-ztBTv_1HBNt?NB7dfz@j}-pkWGO(g9Z+fIDi`t@B{-?j9crNgn-Xwv++WKmP(Cz zDZQ|LT`yB(^j31z)oDNT&d`aA(}yYwma-={^T!^NCI)0a3q7-8&8j_~7aQl>XeHpa zm2K>&4Jy<)mmM@h(;*aBm3=T#@l_E(;Bk0A>fD~5GT#{<8u?)dXliR~fDA*d2M@I< zw#C;UPYn65nqs6Pj3IS%3u0^g%W=OfnIeByZ+%ExdkAsn6EpsP+IC}y+HLhwx~9^e zU?i{=G>{Y%-SB6yvA?`f9XCxpTZm}1Qn=y?-*#$X$1 z-%Zi<*o*tGaVGXK}!U}x%$z)gXl>~oSN&PINW3ER$avV0D?3$a!j~0Z(vIJManU( zNZLigUYX1F$C1%?PW6n2`M5*F{DRclKHz!52@9-@qLJ!Ea@7iu$L{5@%}sR)HsTP* z(Do~H*+7JOf!A=40L9OF_l8?f_1nGoB8AGDuY=~+b7#5gv2casUpL!^g`1Ugk?h4l z*ITat7HcMH0AseoH``oL&4SaiY50<{R_LL-m8Tv2q{N?oUsq0#@072nb?WCkl z-&>>a1zSqA@ral9m(8*0p8=2w3NL&4hQTPung#cs*^TAt7+2CXLO>C(r&gAzW(?|P zF1N|H-F|Sr5gvGYFKS}*!TmvCa&3J7q!R_g`nIun1l0C`lv=$|d&ii6X9Ar+ z0n;bTul3V%DBcHq|-&Kp|$)qD#>^QaoG9&_aCfnKADL1E$L!}d$1Wx z0h=;eMaAFynFuFFBtsnk&tqP@#@pP{kwaH1TuO%Pojtf9`%-*3_!J4Gg$ z-%Z#Q>khNARI#!U@j}W`w0Wrz_pJ3EI8Hi4couZ9WP_XqA?;C<9yVQAqK%J7X$jx9;cjmrk#U09yF_P3)0hXcxThLGsa5VTP?slvQNLfBq8_h(gnkq z3mZV_bGG;Qx`~3+_u%1SDX1VR=p|S3JIpoSicm-xr(ZrWT12%im4XnWNN#H@WfjS6IbMo5pvh~gQM{a@bL?n1 z8vwxKzqW70BcPEv_=}#s@FaR0k87Kxyxq_vxkt@e5%gzi>m8)Kt7?v4lEKgt_A7!`8xWk5Ax_H zmF=IESb!eNLSTY8t2*5C_>1)d6_!bhbtR?dcU$jM1KNl8dI7NqudpCGUvA4YHthAb z8Aa+$hW1+R98a~YM@HoR#Lm6;Or4zN*O#XC%*Plfv!~4`o;Hban`fKP^2s-(>ouDf zd)H6okuLk{&fQve^U{~Wl|FG+>8z+Zxjuyx zUU=aj?0lm(iCz&Q3i$^mcB36YmYm}crS z(a~~rm(ydmNP%8fo}l=uiBe?2yWxK7R|Wk+D{N-eO&^u{%(0g|NIAUEvlDt}9ncEd zRF}zGY0COmcdlW?1Sa}71jEs_wwh-7nRS8jtOtx#CBNtj5(M`g8*=GSaI=pl#*fQ8 z$<^7KdHaD$1PCxGwGN^ZNNbcoux@B4(ePRYWLs&`q4y`9S+6hqLPhk`vioh-={Z>8 zfyU}zw7M1cDwzul-Z~#k_^xh^ypP`a-w`t6$`94JLq~zEPp+GAYi}qb^Bp8!Tbm0z zspr$@t=grN`({v$2w4Cf<$9Bpw2P%Cg-aN;{#tkKI)e!b*at82xaf3k(g)GmpYkr}zb-M|OuJiaxg2|_J)||s9&1hur(~2~X)n4bH3Mh3Pu=&O z`9(G}6fqPM4<%PSd-A;aB7VL93(UGXI2>33_$DEHOBHetnKbJS-Hy@_LFY#|#B8qg zlO4ZZ0QvFA+5eT=|K7w)S2>d>e#K`=#-+o-gUwp>hB?s5wW5@sEGx8fNxxOTceCm8 zSVZY)iU&BQZZUj9IPXK3Tu!nlUyJw?X>(yYsry;z-Mg0V2MHQ1gY&KL=}hfk8AQgk zJ;}2a_Ij!hnUP7VT*v~;`M2|soJq?2?k{jfRNjPLGR^4o>u1k#X)iNJx1AWF3q2xX z^Qt{>FC>A1D8=62^}Kr3(|_o%D!gTwERB;tCjM!%l}T?|Xf66-sg8X7e^i}ySd`oQ z^(hgM1|5?2~-aY5{J@NkUb*_s! z&$FL?`~PWEmjbM za8&fHWc2MlwRAM~x4Qx`J>`c=EO9bo1cHpy5kC)4lAsqo%h$oc>6H9@d~qE41mLp~ z1)b}0_tP4cA)~)fZ@hBrZTCo?4L%!SnVJyEu|-aUMG zV_SC}Xq4kkYAr^dr8e}9P*%yVr=mG$LiGA|WMV=^smvoHJf6J+3)?N`lvz=L!_)Iq= zNAddY>&Gc`tyGe^rfWT`UUb>eAwz1E!06EMP9PDJ)?i9chb*zPT|EVO=ccb$7n)xt z#xvPBLE}qH>5%R_ninVQB4lFc$STAX72@Vh3ZO3<0)0P^c=H0NSowi5Kr zx_cmv^@znMK>`@?yF5%zZ1MYvJaIh8-t|P-P44~EcH@$s?^6TGo&benjvHx z#5Pi`JVPfgS2`FYz1=gpIa|8Hd|3FS zKRDYOZ(|Z)*|~D}qlMZ{}cNO3Xb4`Uc2M+8w}v__A9YFTaK4`I$u^W6a?84}6$vn>GYr@BTAS zS2yP!l6PT27GvhCKVL}@X?klU=Lv@HtOpwur_Rb=1wZ!Yn0we%r#;Izkc4|!zIOjD z#r@>oqEE@P&(b%trhAleX_O#O2fA0N_Ou?};y53)GsqqrY^OYg%Iyk`=!U9Tl>=pX z3_U0|EsJE@ir5NPs7IS)aOfx`y&lu3Njd-INSY6x@iyc3&S0u@XuDu}@U4Ta4C^9f zUZScTxc~5k0&m^3`A?8X(~?WDe?2~#@?!iQpOxAi+6tS@< zVbq=}^`|vxx_;p@mQ*H{&@`18RvR}HuP`6O`Wa8KT`pAm9m^~UFZ&?SP5Yw+cX_8m zK$O`>c1E6HicZlp?h)rC)Oq=&t^ehB((wed5hY%8jrA9Y_&Js~UpJ|h>11${8(FeX zZDuGj!$Sj0#fT;Vr;~bxj$_H!vsZ&&v9@c_nLkZ`c;F$mDxSG`<-J<@))LH$ZFPrK z$rP4H5(G|sd~lkL&s$@+oQ)WJJU&?F&sbddGZ?pBg+_-t-rP7a_WSaVY$Y3+Ie!d? z+Pv0C+geBJpBx}Wqsg|TP_b1n>5@y^#ha;ioTUf>32H8-mtG zB)Vl$MzOs7--zF>eZ078iH0&KZaSCq9+1wbkWZ;cH~2TVdW9b7p0f7c+`>74Z5VLs zvJmC?pU;}oT`&U#mep{;jVv$#3RyOKW`45G5$K8rE`mkT3`)g&P9t2C!sj0{%4F*F zEnZo~Qv_Pm<b9dQwYmg+K!X2(vH}z7%{>m|uzg?z=YL zVC+?PO|-D!I8=ocE4W*uZ*5VJl^~8&&+-@x-p;0m^qlS7Ox3ejyrm&FnoO#xlx&|A zcy)`Dw=W9!hZe6tbV>Fr6WtI8C;`d;r0P5?D(i8Sksw_z>0`|AVf}O8Xx>jOAfO_2 z#@4VZ^BE3iPP_03`BS4Bbic%!T&QHzS+l^`vhKFUs{qGt%>(erTqO0^>*=Mjsoh!J z>OSN?G<>f_%Qj)7F62&lp#TB{HS7pul5wB?#hu8Q=b4wL4+HyQd(wi}3BM0Q5>Kx% zZT_$}UH3M#(T~GI&)U+TT^3G)k4<)$RTQxU=X`6`LdwqS$yCbR zWwz7`PHG?$v%g4!EFI?BcAR{1*?p;5^^GUOQ9z3}p^GuOZwdXz~Y!F@?5Zu_uAU% z?Kc&X7xJvh-`^bPta-0&irer|KDDi_02Toli*Yf(;f}rU5E-1=G$^ zO-mG=9Z%7jxt_c-RCk{Ev|)q)Q;U~o!DaIb{rukM=sO}N71vfi((~MTx+5st;pJD; zVx9byFkX`(gn5=5sG%fZrg2dpMKG9Qd_aRt_GvzT*uSxH_rL*(yL8vmgY*wuSq2=5 znjG~ryQitG9m$cg-ZH^QzCj=KTfjYr;owYaU}z87O1b4?n`7cLM1@fP9(ok7ldr1c z#I$6@L)kU41FnHFUdFFwug_lrptCy(6}6SKC9qmUwXO`W@d)=?DIU9vms!wSzptmj7&%(ST| zoto|N#*%B^>y_2MP-Xy4{!x@^p|(Yn#}jT$*2kUhqK-m(Cv0GJ3SbFe%M3^E{GR$j z`G#1YGCwUXdwFsIh^g=fy}rRYDhL(a%s9Xet-{7+m869E$Ybyi|=c57$hz(VNKUv=c9p>P@;bdPBy$G)+_-mT=Q#wt_F zk$^Y}iqY4k17#D%q~CqTM<`MjO}tE_d2m7#cF1x1VosUHMPfygUQfJCf$yKuyVx|Y z?^?h7-+}n}-4+R^`K7yW&ftlv#m&PFXy~X>MX-f%gk!#>MEOyaA_QO8j*5n~ij zYEBF|1&4_$#YHx?*j|4ydo=raW{ zM?D&1`!l3IRd@FEou-ulz`$EX3OV!Viwq_2o|LZ{Hd5$tykLc-bUtItqBrzOCE+f7 z3;{5YPd+3`_QmbHLfo4C)))q1;Jt=K+>q)w6(z;v-N|QfYb(oj(emGK>Twzx?FSaz zi7dj!g#Xrz*Umj3c({bdHiM3l8oHNKLN@ zAC$tV9;F3}JBXRmy`|)lQP%bGy0*Vv-)2ryqmI&S&rIcszeYz9q}|^Adz(etZIr)P z5!r#Cz;`e)-oVgpW>#|9!$W%6`rpYBL;_22--uX2_iXoJV8NpX_uI?y)cZ6K&`E1Y zgZ$dn)XgpMJ>|Rfn!3~Jiph(HV@YmFE`?P*YF$WYhwv)!&yCs1yL|*k63yok`A%MR zaL6e-YR*u3H5xR<)cq7;4Sy9m+DtxFZ7dE0OuY{7bqls*x|c-}TdWF+qLTvw+V{H1 z*In4e-=5vVQoSK*je{yXQN8z?opNj|IQH^biS8A6pl;sqSVm+C0R42c%nI@{UseW$LU3yE9SXA{Pv zE{WQ`9r7&Hd$kq1L9q?{(|^T63Zbu@0k|Cqz!R5bO!mRvI;MSo!r!$MC(rh&egmNq z%EdT@anZ;19s6}yDAeM{@s6sPv^Ulng zzPu?X5)snrHZS_yG{BISxO%uU7lm1|oid-3ny(MZOH^aKFFKm4O;v%=*Yi0}9|DOPr+<>%u%3qw9Baj;Mkk{QU~sBDIlwHmvNl|`O5 zkD80bF#g`OWL1`tpp0b(puiB@=ab#ctaTLqv|$a<>d&Rm2F>gJ*Jxp*QOA559AWAT z!(@p zT)_6x9V-|Z7gwLGkKAL_A$tnBrC|Fs+@hv?b06LR$7QGkcCEkv11~L&LvJ} zH?vx*+1j7>`)=(&k}V#5fkbt1Z3@@)4u*kG>eM39{f#8O@8Zq^Iv1wPjMp+b8-oFg zHz#VTXq;Cl(Xq}9XQgOAJ3dy3A^nl%c7tK$?SbQkabMR*dCUSmzqB-jMXy|l=#xM# z%+y6I2$ddz_L{&R-WGf??=!wC_8NdbQ&0|rj$LA)WjVyGCEP*8>>V5$du#6hJuaSW zH=n+Vs?vYLEX;y zYPvDBcV8BS5i7(bN4TkhXK>Q$sV1DiiewS*PnlXY^>T_T>5kR|(6dpJJJbV|Ta zF++_>R|i;VCzU*q2P=qWlJu)c>7ZHqzVY~a=&N8xV1(K_+RN(iTKOZNvvZ+%xSeP$ zEKc!kG!F!=6EQS#HSH*W3OW1euSAzNeo2!BloyxoLML_}-@42bJpu6VQl|A-C_L~+%$ z=P#Q-TR@V0w)`?}v`Kz&$R4$=bLIn_VBxb& zDztqF{ssw3w>0q?Rw*9RIA8<@2^bWug>;2M69NmdgG|c#tr>kvIj$N*NdN9Bb7d5F zm79;8?51K|RN+Rh_Ez~ZU+Y=u#|tWJRR{0h*@=l!UeQD-r5S2YOtA#2rj-4(-n6z7 z!>_Zq5|UDD$bE-}yp^0ye_#%9w^l`0W<|dR@GR%CgI@(mT;c#*ibmYq$Jgq@W6*ez zUdiZu^8(c_mX__>N*Bft_oh?Z560yIr5&Kg0GRscyKA5`f+%e{A_B0)eQ^CsPrX zXKB3viFvzg3F_AXtKm1PcNM}jEdu-V|%$(>l*eN{67Bg&`81cP5YM!F&PR2xE^J0(HVk~4+IJ3KVe zT96R-LQsHrDTrPXMcyKCKi7azFRE0+A?Nig{GS%>3Tu>S;&9WwCb*M_bVArZ(o@Cc zqvOS7iHE1B|1HqEp*Ltk+~AwQ7jgXX*wv;{m{KltGF@WPv!jV>3?Xfve9>_uoR&?? zgpc`pyVu0UJHpTwkpxD+hURUj7q(R`F74iYO{>W4`&%i~i>6Kv!O)VRM7~M*7X2T~ z+K|Nf!9lKumEEL~4T@Yp!IJ(!8sv@{*aC zosPJ^%LiPa#MlXH`>*b2UiIfKYWPUdP`5Fin*NFL1J;h3ynQ^=o-s~=t1=r@1v@T6 za$Xt`ZIP@!cM4S76eWCQp0wV4c}mFt86BKkAjNj-n>>pigS`dN5dG5`+pHSa9)LpD!TR-i zGJh4t`Q~X-Ud%`I{A?77R$y*G-RF=zCMHX+csvM>AHn*4C#s(B@6!H-GFQ z>yxEBOSu<|pAm*(YuW53b0=7Ta;q15hecg3B<!8kL3Cza#YnqfI;+R^tAn)6GmIUnNDC)Ux zzae=?a=V=R(Z_H{AD?`!Y_&UtagWO9w5*a`UZ%wJ;OK6Dkl4<)0Pl>(TeWN)pK@G` zoCk-a*;{x9wk~R_l=o`!R=hl+)vq7zPQ$Fio;aunRD@q9M6mr-NcmGD`gwB9jU-k3 zbiT?-puqSJ!;frmjNB7L7uVT}#yvC>@^TIKPv_Qb>7G@w>A;Z_wME#^GM_~K#SozU z@%`PSDA!~EIY9c8*(AdjyaR!4+hbJ_+a-DrDa^K@(&(B5t{w6%K#S5|Orbe#u>+3g z*8%SGmhRmNaH)=!0Z*1!b4~D;<3!MSutOUxp@pf{{Sdvkkq7?he}Vnn#~SJZYoCT+ zK`pFp@Jh&?Xg6#!1qA|BNi7N4gB~3E$^cjW{AZ|b8V@=b)f=Cg^=$WwO%;L!bWg4# zfzF7s;w<5I1MQmola33NJjqoh+Xl6>Ww*#cSsZ)5va`?+YCnri-S{UU0ryOWq4EXA zPf2kQWd_!jX!HX7tUeCkNhh>C_ES!3X2&LNS_ zaJ?5gn@5R^(W_bobHkCBO%7iJG7vPzOa%oqb~SY_?Wh23KVS!ln7R9u{oH>)FJkM} z8nfQDkLTV)UQyv-y=RXu5e8C{$m-3ymPG>c`O6JdM~$f;_L^6hQ@jo0q~IoynDib0 z208R;W&OqWA~1iANdEk7Xgq!g2Rzn`ox~~vDY|Uuw=U?STi=bv(@~+dQ(W1cjndc{ zZ&dX1>#v#x2{XZx=wHpCEC%G(Vxh#8vdh=tTtCm}r8Ca9h$nnnbDuSa1GGpK0Ah;8 z@b#;a>sRc0ps*Sw_dGwb&4@`Bfmn}W8324oX6OpPw)e`?5bZ&99&9!mS?8^TcD_U3 zPTjgLSyo0Hqhc^z0l( zRPv?6k@+V7r_5Pz8Ol)qPTzVoAc;Zohk}5SOI&iqmH>6hzriO!8d-#cM+sH_o8;^z zS4WTVTdy-VT+d_F0Q;|13A3}0y_=TSbv?rW1zRvK^D8-GDn~-Oyd^TTi=j>eo6L&T zCqMdB3mg{j{TLWc-Lj#}pyEXr14Qj-SBKemjtW{~GV8?$bQOV0W}wKD(hPK{3Gp2& zDlyyjWA_e=mA=h%Zb;-~nlBWR77DnQNE?g%;AZ=Uu}8#a0ySbi?;xtsVE4F7w0RFl zh2iH`9#7LzCKme!l+J?N7kJFDI@)$B1eKrtrv1IGEgYXHtdH!CaYpe(drK@Slxo{4 zT40cxmt3Y#WtQ=$CtS?_wsCn5HcXOM0?2=U=TF}u0qEEteyZ32NS_h)f zke7qc{WcMUAm1dyq{I-q)|{qNa@wPP@s1&k^I9|Ja(gsHuRx`gbxA2JPE)R;)XdaQ647iOQNnipb?0B-nf%e z-Ji#k>v%pSXUA)#v;Yi5W|km7Cp}Pbaeg~~xXN7iW6$=SM`6O zy_Cp*6blJ|aB~xn!sOzfRr^g^*e<_DaFF6P*)}~c6eR>IG*vd*3o%b&{0W%grlYIz zke%fyQO0`_`6IV4pMx}@IPoP>c?pPPDL$8K6*G<1A~RVnk@vaW75WAWCFDDhMO616DRtCs8&@brh0!eWvLzR zlgrzVe3GIH5S;PTdl?zhVF8x-$W>H4Ypv{srHi*T|7=?{7Ew2rvA%!EhX%cY4PuD- z=S$dh{%flXwxC>JuWq0Ox7A;xD_&Lq_Y-&7{aBCiN?k>=KXB$2yuEi*jrYFMFAftj z;UhkMR6E(7ehVnz$KE*4fZASWe7Hd8jBi4oo@vEh(h++jWt|_xLXux^{{c@MxXLJ( z_BT591b{i|a41MTB+BWgA&e#3OoGMBq)4>P7~H4eSSB&h_Z4JU$k0VFm-?gP*ldImT8A$+-19oSBTVEmi*5W&iJ&4*e(}&ze zt1orlvDQcaIZl4=8)U$!G8`%ZH3ha8Y#-i?J1_wC9|g;1v~)5pQpE7*-}vu4f5_4E zjsvxT5-=CFy7fzTL?QV_>l2)QP#kCMd&RKzAKVi|nh=OyxpT`bycctR^^J-q0KaCvf+?>wP(PD<2lr$e+EbqbJp9Q zv!A{c^*}x#_z6!M{yYx8W_0I^13@cE;cnXvQK6U^=M+pkAx*ED47$7apvtaTJ{y$r z?nZ_3sTa~7zfWIxVlrlJSD!@!P_FnH-mml9Fv_M4L3oNxfRZRJkg*!qwQMd@-7&4D?pKQKwAOg%d; zaJ}K4?8$edN|;`GH?S7IjKe9W!~S`n;NL**+22Sy*U0wEi)>ISkSFN~TJm&OFbd)< z@Jg6xUAhls2CGKfzPX78MBqgJbTq7bRm!x`vQ?smo6dIMEA`Jlf*nX$g~pqslMu|b zc^g2BSg}a5YPNfVK5(peF{5IfZ|ogTcUQJ-_84kIlH<)k?@o?@Q3zm%yvQ3k?!wy; z?}QrAewUcoKy9b*y;u04Fk{)K9E4qx-?9jCY5ra~UcAgZt9I)~rW7x|vNtDbBM)HF z8E#XjP*Vvzps76rr%Fu(cE+5yPyV@NBM6@?L0#axfzwpytpX0Ac-y zN+Q2Fvct$EoE>6e7}VzXR3Yhhs_1#y+3y_>ZAJGn!@=lOQ~z=ogd)DFjH6Ehbb#m0 zskZ8VPe3Q1xhKh4ZO4g(1r4a*&Q_LNF0y$uYEwg}_Ov|lD6*$WA<9coE*ONMl3x=? z{Y2wV+46MuOE0AbIrx0P2#*ePR<_UkZ4n9c1VV=P8N|h3!o}>@hrSAAP*K_;>XyPl zmoYy$Nb?grdX7F_P%st?#Jl1aBvLLtNc?O5sev^a3SYMEJwLu3PqD#_93LwMKu2By zb=E<1p>C%o*e4K9nqDrX3An)}3*k%Sy61@B9T`I~br4QwR#xct&px0z@0FiafqeT^ zcd`Bbc4mfA-sRt=Wx4G!+^@`MS|y5oAcvz$5Cs&n`dwVnMV>M^@qIwmx>+T*#9nKR= z_T^Pmj!i5z?_D
DEn0M5n7e?nyuI!$ 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

Mbf~Thz@eB*a?ybNdbMyFHn1)`pQ`O7+Lv1yK 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 HColgGk``ev literal 0 HcmV?d00001 diff --git a/robbery/master_pol-module_1_2/app/res/styles.py b/robbery/master_pol-module_1_2/app/res/styles.py new file mode 100644 index 0000000..e76ca02 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/res/styles.py @@ -0,0 +1,35 @@ +from string import Template +from res.colors import MAIN_COLOR, SECONDARY_COLOR, ACCENT_COLOR +from res.fonts import MAIN_FONT + +styles_template = Template( + """ + QWidget { + font-family: {MAIN_FONT}; + background-color: {MAIN_COLOR} + color: {SECONDARY_COLOR}; + } + QPushButton { + background-color: {ACCENT_COLOR}; + border: none; + padding: 8px 16px; + border-radius: 4px; + } + QPushButton:hover { + background-color: {SECONDARY_COLOR}; + } + QLineEdit { + padding: 6px; + border: 1px solid {ACCENT_COLOR}; + border-radius: 4px; + background-color: white; + } +""" +) + +styles = styles_template.substitute( + MAIN_FONT=MAIN_FONT, + MAIN_COLOR=MAIN_COLOR, + SECONDARY_COLOR=SECONDARY_COLOR, + ACCENT_COLOR=ACCENT_COLOR, +) diff --git a/robbery/master_pol-module_1_2/requirements.txt b/robbery/master_pol-module_1_2/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..bbd2d4fb43edf912a1993ef41042c99ef08d5802 GIT binary patch literal 138 zcmezWFMy$v!54^w83Gx67;J&ikUr)Ib$IuC0_tD01cDh9~P4%Ph|<6(7w>J(6G^ulbKIcvQPvdvjkB5 EAEf>t?*IS* diff --git a/service_requests_v2.db b/service_requests_v2.db index 977ae1868491f7b4039dd9c19aacd347d3d1a33d..7c9edb620c61f2b49f82fe8b2a717ca440a9a554 100644 GIT binary patch delta 593 zcmZ8e&ubGw82x58iMyKYbfGawJgi_TQDZYZNn3JJELgm>SkD@vE?5oN>?RfT;BF}N zAheo2t5oU5({5{GHKY|hN^usvcrsW201t?BST1%Eg-+h7WqQ@&=(>+M{R2($krnBv9C=1u=c!8aF2`!9G%>ZNG*SVh7 z37_(PzQ_A~z`Fvz_h$a)IiqA$ij)?ONuAQFQLY+>Tfw6+MaK%K^7%7D0e}I>;Qg2z zpT1+REY&aHHS6_@7Jn5we3uWxHt&Ycjk<_$P5wF5VC81(K7S$nwz(f}2(O7;%!^e^ z(*Wpd(71v+BZ-kzMg_8}{#G4T%B*G7pmCUf@-QWXf@WQXVZJHU-t%|d7wLC-SLb_S zJ6sdyZ83WJ_b$dd-;OOi5Y!L4$m{bUq6v2B{*#L(i&}V;jEtpu_n3!j3l}>7kz?;! zwNieZkO3*PuEwypUb`npZAKgJIJZ`{6e0&e4#a