Compare commits

..

2 commits

Author SHA1 Message Date
c6917dd85e After Graduate Update 2025-11-26 19:31:33 +03:00
b92a91ab37 Final Update: second variant complete 2025-11-26 12:56:14 +03:00
73 changed files with 10479 additions and 62 deletions

1302
control1-2.py Normal file

File diff suppressed because it is too large Load diff

902
control2-2.py Normal file
View file

@ -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())

View file

@ -5,10 +5,10 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem, QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem,
QPushButton, QLabel, QLineEdit, QComboBox, QDateEdit, QPushButton, QLabel, QLineEdit, QComboBox, QDateEdit,
QTextEdit, QMessageBox, QHeaderView, QGroupBox, QTextEdit, QMessageBox, QHeaderView, QGroupBox,
QFormLayout, QSpinBox, QCheckBox, QTimeEdit) QFormLayout, QSpinBox, QCheckBox, QTimeEdit, QDialog,
from PyQt6.QtCore import Qt, QDate QDialogButtonBox)
from PyQt6.QtCore import Qt, QDate, QTime
from PyQt6.QtGui import QFont, QPalette, QColor from PyQt6.QtGui import QFont, QPalette, QColor
from PyQt6.QtCharts import QChart, QChartView, QPieSeries, QBarSeries, QBarSet, QBarCategoryAxis, QValueAxis
class FitnessApp(QMainWindow): class FitnessApp(QMainWindow):
def __init__(self): def __init__(self):
@ -299,9 +299,11 @@ class FitnessApp(QMainWindow):
stats_layout.addLayout(stats_form) stats_layout.addLayout(stats_form)
# Правая часть - диаграмма # Правая часть - таблица для статистики
self.stats_chart = QChartView() self.stats_table = QTableWidget()
stats_layout.addWidget(self.stats_chart) self.stats_table.setColumnCount(2)
self.stats_table.setHorizontalHeaderLabels(['Зона', 'Количество посещений'])
stats_layout.addWidget(self.stats_table)
stats_group.setLayout(stats_layout) stats_group.setLayout(stats_layout)
layout.addWidget(stats_group) layout.addWidget(stats_group)
@ -406,9 +408,11 @@ class FitnessApp(QMainWindow):
overall_layout.addLayout(metrics_layout) overall_layout.addLayout(metrics_layout)
# Правая часть - диаграмма доходов # Правая часть - таблица доходов по типам абонементов
self.revenue_chart = QChartView() self.revenue_table = QTableWidget()
overall_layout.addWidget(self.revenue_chart) self.revenue_table.setColumnCount(2)
self.revenue_table.setHorizontalHeaderLabels(['Тип абонемента', 'Доход'])
overall_layout.addWidget(self.revenue_table)
overall_stats_group.setLayout(overall_layout) overall_stats_group.setLayout(overall_layout)
layout.addWidget(overall_stats_group) layout.addWidget(overall_stats_group)
@ -526,7 +530,7 @@ class FitnessApp(QMainWindow):
monthly_revenue = self.cursor.fetchone()[0] or 0 monthly_revenue = self.cursor.fetchone()[0] or 0
self.monthly_revenue_label.setText(f"{monthly_revenue:.2f} руб.") self.monthly_revenue_label.setText(f"{monthly_revenue:.2f} руб.")
# Диаграмма доходов # Таблица доходов по типам абонементов
self.cursor.execute(""" self.cursor.execute("""
SELECT membershipType, SUM(cost) SELECT membershipType, SUM(cost)
FROM Memberships FROM Memberships
@ -535,17 +539,10 @@ class FitnessApp(QMainWindow):
""") """)
revenue_data = self.cursor.fetchall() revenue_data = self.cursor.fetchall()
series = QPieSeries() self.revenue_table.setRowCount(len(revenue_data))
for membership_type, revenue in revenue_data: for row, (membership_type, revenue) in enumerate(revenue_data):
series.append(membership_type, revenue) self.revenue_table.setItem(row, 0, QTableWidgetItem(membership_type))
self.revenue_table.setItem(row, 1, QTableWidgetItem(f"{revenue:.2f} руб."))
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(""" self.cursor.execute("""
@ -598,40 +595,21 @@ class FitnessApp(QMainWindow):
""", (start_date, end_date)) """, (start_date, end_date))
zone_stats = self.cursor.fetchall() zone_stats = self.cursor.fetchall()
series = QBarSeries() self.stats_table.setRowCount(len(zone_stats))
bar_set = QBarSet("Посещения по зонам") for row, (zone, count) in enumerate(zone_stats):
self.stats_table.setItem(row, 0, QTableWidgetItem(zone))
categories = [] self.stats_table.setItem(row, 1, QTableWidgetItem(str(count)))
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): class AddClassDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Добавить групповое занятие") self.setWindowTitle("Добавить групповое занятие")
self.setModal(True) self.setModal(True)
layout = QVBoxLayout()
form_layout = QFormLayout()
self.class_name = QLineEdit() self.class_name = QLineEdit()
self.trainer = QComboBox() self.trainer = QComboBox()
self.class_date = QDateEdit() self.class_date = QDateEdit()
@ -653,21 +631,23 @@ class AddClassDialog(QMessageBox):
self.hall.addItems(['Зал 1', 'Зал 2', 'Зал 3', 'Бассейн']) self.hall.addItems(['Зал 1', 'Зал 2', 'Зал 3', 'Бассейн'])
layout = QFormLayout() form_layout.addRow("Название:", self.class_name)
layout.addRow("Название:", self.class_name) form_layout.addRow("Тренер:", self.trainer)
layout.addRow("Тренер:", self.trainer) form_layout.addRow("Дата:", self.class_date)
layout.addRow("Дата:", self.class_date) form_layout.addRow("Время начала:", self.start_time)
layout.addRow("Время начала:", self.start_time) form_layout.addRow("Время окончания:", self.end_time)
layout.addRow("Время окончания:", self.end_time) form_layout.addRow("Зал:", self.hall)
layout.addRow("Зал:", self.hall) form_layout.addRow("Макс. участников:", self.max_participants)
layout.addRow("Макс. участников:", self.max_participants)
widget = QWidget() layout.addLayout(form_layout)
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) 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): def get_data(self):
"""Получение данных из формы""" """Получение данных из формы"""

693
control2.py.bak Normal file
View file

@ -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())

Binary file not shown.

2461
main3.py Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

6
ressult/.env Normal file
View file

@ -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

Binary file not shown.

Binary file not shown.

60
ressult/app/database.py Normal file
View file

@ -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()

48
ressult/app/main.py Normal file
View file

@ -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"}

View file

@ -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

View file

@ -0,0 +1,5 @@
# app/routes/__init__.py
"""
Инициализация маршрутов API
"""
from . import partners, sales, upload, calculations, auth, config

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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}

View file

@ -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))

View file

@ -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)}")

View file

@ -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))

View file

@ -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))

View file

@ -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))

24
ressult/config.json Normal file
View file

@ -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
}
}

196
ressult/database_init.py Normal file
View file

@ -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()

9
ressult/gui/__init__.py Normal file
View file

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

253
ressult/gui/login_window.py Normal file
View file

@ -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()

574
ressult/gui/main_window Normal file
View file

@ -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"
"учета продаж и расчета бизнес-показателей."
)

574
ressult/gui/main_window.py Normal file
View file

@ -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"
"учета продаж и расчета бизнес-показателей."
)

View file

@ -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"
"учета продаж и расчета бизнес-показателей."
)

View file

@ -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

344
ressult/gui/orders_panel.py Normal file
View file

@ -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)}")

193
ressult/gui/partner_form.py Normal file
View file

@ -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)}")

View file

@ -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)}")

View file

@ -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)}"
)

11
ressult/requirements.txt Normal file
View file

@ -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

17
ressult/run.py Normal file
View file

@ -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'
)

51
ressult/run_gui.py Normal file
View file

@ -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()

View file

@ -0,0 +1,2 @@
**/__pycache__/
.venv/

3
robbery/master_pol-module_1_2/.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View file

@ -0,0 +1 @@
main.py

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.11 virtualenv at C:\Users\student\Desktop\master_pol-module_1_2\.venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11 virtualenv at C:\Users\student\Desktop\master_pol-module_1_2\.venv" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 virtualenv at C:\Users\student\Desktop\master_pol-module_1_2\.venv" project-jdk-type="Python SDK" />
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/master_pol-module_1_2.iml" filepath="$PROJECT_DIR$/.idea/master_pol-module_1_2.iml" />
</modules>
</component>
</project>

View file

@ -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 <user> -p <db_name> < app/database/script.sql
```
- Замените `<user>` и `<db_name>` на свои значения.
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/` — ресурсы (цвета, шрифты)
---

View file

@ -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)}"
)

View file

@ -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)
)

View file

@ -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}")

View file

@ -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 ;

View file

@ -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

View file

@ -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()

View file

@ -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,
}
)

View file

@ -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()

View file

@ -0,0 +1,4 @@
MAIN_COLOR = "#FFFFFF"
SECONDARY_COLOR = "#F4E8D3"
ACCENT_COLOR = "#67BA80"
ACCENT_COLOR_HOVER = "#529265"

View file

@ -0,0 +1 @@
MAIN_FONT = "Segoe UI"

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View file

@ -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,
)

Binary file not shown.

Binary file not shown.

BIN
service_requests_v2.db Normal file

Binary file not shown.