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 8472fde..6659418 100644
Binary files a/fitness.db and b/fitness.db differ
diff --git a/main3.py b/main3.py
new file mode 100644
index 0000000..553ac1f
--- /dev/null
+++ b/main3.py
@@ -0,0 +1,2461 @@
+import sys
+import sqlite3
+import os
+import math
+from datetime import datetime, date
+from decimal import Decimal
+
+from PyQt6.QtWidgets import (
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
+ QLabel, QLineEdit, QTextEdit, QPushButton, QTableWidget, QTableWidgetItem,
+ QHeaderView, QMessageBox, QDialog, QFormLayout, QGroupBox, QTabWidget,
+ QComboBox, QDateEdit, QSpinBox, QDoubleSpinBox, QCheckBox, QStackedWidget
+)
+from PyQt6.QtCore import Qt, QRegularExpression, QDate
+from PyQt6.QtGui import QRegularExpressionValidator, QFont, QPixmap, QIcon
+from PyQt6.QtSql import QSqlDatabase, QSqlQuery
+
+
+# === Стили приложения ===
+APP_STYLES = {
+ 'primary_bg': '#FFFFFF',
+ 'secondary_bg': '#F4E8D3',
+ 'accent_color': '#67BA80',
+ 'font_family': 'Segoe UI'
+}
+
+
+# === Функция расчета скидки ===
+def calculate_partner_discount(rating, total_sales, sales_count):
+ """
+ Расчет скидки для партнера на основе рейтинга и количества продаж
+
+ Формула:
+ - Базовая скидка от рейтинга: rating * 2 (макс 10%)
+ - Бонус за количество продаж: log10(sales_count + 1) * 3 (макс 15%)
+ - Максимальная общая скидка: 25%
+
+ Args:
+ rating (float): Рейтинг партнера от 0.00 до 5.00
+ total_sales (float): Общая сумма продаж
+ sales_count (int): Количество совершенных продаж
+
+ Returns:
+ float: Размер скидки в процентах
+ """
+ # Базовая скидка от рейтинга (0-10%)
+ rating_discount = min(rating * 2.0, 10.0)
+
+ # Бонусная скидка от количества продаж (0-15%)
+ # Логарифмическая зависимость - чем больше продаж, тем медленнее рост скидки
+ if sales_count > 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 12f3f3d..6ff4e21 100644
Binary files a/masterpol.db and b/masterpol.db differ
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 0000000..a03691d
Binary files /dev/null and b/ressult/app/__pycache__/database.cpython-314.pyc differ
diff --git a/ressult/app/__pycache__/main.cpython-314.pyc b/ressult/app/__pycache__/main.cpython-314.pyc
new file mode 100644
index 0000000..b1c1369
Binary files /dev/null and b/ressult/app/__pycache__/main.cpython-314.pyc differ
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 0000000..b4c160b
Binary files /dev/null and b/ressult/app/models/__pycache__/__init__.cpython-314.pyc differ
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 0000000..0ab0a5d
Binary files /dev/null and b/ressult/app/routes/__pycache__/__init__.cpython-314.pyc differ
diff --git a/ressult/app/routes/__pycache__/auth.cpython-314.pyc b/ressult/app/routes/__pycache__/auth.cpython-314.pyc
new file mode 100644
index 0000000..76a68b9
Binary files /dev/null and b/ressult/app/routes/__pycache__/auth.cpython-314.pyc differ
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 0000000..cf8e18a
Binary files /dev/null and b/ressult/app/routes/__pycache__/calculations.cpython-314.pyc differ
diff --git a/ressult/app/routes/__pycache__/config.cpython-314.pyc b/ressult/app/routes/__pycache__/config.cpython-314.pyc
new file mode 100644
index 0000000..f2a5e23
Binary files /dev/null and b/ressult/app/routes/__pycache__/config.cpython-314.pyc differ
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 0000000..0a432e2
Binary files /dev/null and b/ressult/app/routes/__pycache__/partners.cpython-314.pyc differ
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 0000000..77df468
Binary files /dev/null and b/ressult/app/routes/__pycache__/sales.cpython-314.pyc differ
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 0000000..a09ba33
Binary files /dev/null and b/ressult/app/routes/__pycache__/upload.cpython-314.pyc differ
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 0000000..844ae5c
Binary files /dev/null and b/ressult/gui/__pycache__/__init__.cpython-314.pyc differ
diff --git a/ressult/gui/__pycache__/login_window.cpython-314.pyc b/ressult/gui/__pycache__/login_window.cpython-314.pyc
new file mode 100644
index 0000000..30f372b
Binary files /dev/null and b/ressult/gui/__pycache__/login_window.cpython-314.pyc differ
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 0000000..bd6114a
Binary files /dev/null and b/ressult/gui/__pycache__/main_window.cpython-314.pyc differ
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 0000000..b5a2627
Binary files /dev/null and b/ressult/gui/__pycache__/material_calculator.cpython-314.pyc differ
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 0000000..63a8845
Binary files /dev/null and b/ressult/gui/__pycache__/partner_form.cpython-314.pyc differ
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 0000000..8bb9689
Binary files /dev/null and b/ressult/gui/__pycache__/sales_history.cpython-314.pyc differ
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 0000000..9744b0a
Binary files /dev/null and b/robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico differ
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 0000000..c192a72
Binary files /dev/null and b/robbery/master_pol-module_1_2/app/res/imgs/master_pol.png differ
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 0000000..bbd2d4f
Binary files /dev/null and b/robbery/master_pol-module_1_2/requirements.txt differ
diff --git a/service_requests.db b/service_requests.db
index 65c3422..569cec3 100644
Binary files a/service_requests.db and b/service_requests.db differ
diff --git a/service_requests_v2.db b/service_requests_v2.db
new file mode 100644
index 0000000..7c9edb6
Binary files /dev/null and b/service_requests_v2.db differ