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