Compare commits
2 commits
703adc3326
...
c6917dd85e
| Author | SHA1 | Date | |
|---|---|---|---|
| c6917dd85e | |||
| b92a91ab37 |
73 changed files with 10479 additions and 62 deletions
1302
control1-2.py
Normal file
1302
control1-2.py
Normal file
File diff suppressed because it is too large
Load diff
902
control2-2.py
Normal file
902
control2-2.py
Normal 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())
|
||||||
104
control2.py
104
control2.py
|
|
@ -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))
|
||||||
|
self.stats_table.setItem(row, 1, QTableWidgetItem(str(count)))
|
||||||
|
|
||||||
categories = []
|
class AddClassDialog(QDialog):
|
||||||
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):
|
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
693
control2.py.bak
Normal 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())
|
||||||
BIN
fitness.db
BIN
fitness.db
Binary file not shown.
BIN
masterpol.db
BIN
masterpol.db
Binary file not shown.
6
ressult/.env
Normal file
6
ressult/.env
Normal 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
|
||||||
BIN
ressult/app/__pycache__/database.cpython-314.pyc
Normal file
BIN
ressult/app/__pycache__/database.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/__pycache__/main.cpython-314.pyc
Normal file
BIN
ressult/app/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
60
ressult/app/database.py
Normal file
60
ressult/app/database.py
Normal 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
48
ressult/app/main.py
Normal 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"}
|
||||||
75
ressult/app/models/__init__.py
Normal file
75
ressult/app/models/__init__.py
Normal 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
|
||||||
BIN
ressult/app/models/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
ressult/app/models/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
5
ressult/app/routes/__init__.py
Normal file
5
ressult/app/routes/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# app/routes/__init__.py
|
||||||
|
"""
|
||||||
|
Инициализация маршрутов API
|
||||||
|
"""
|
||||||
|
from . import partners, sales, upload, calculations, auth, config
|
||||||
BIN
ressult/app/routes/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/auth.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/auth.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/calculations.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/calculations.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/config.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/partners.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/partners.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/sales.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/sales.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/upload.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/upload.cpython-314.pyc
Normal file
Binary file not shown.
45
ressult/app/routes/auth.py
Normal file
45
ressult/app/routes/auth.py
Normal 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}
|
||||||
43
ressult/app/routes/calculations.py
Normal file
43
ressult/app/routes/calculations.py
Normal 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))
|
||||||
32
ressult/app/routes/config.py
Normal file
32
ressult/app/routes/config.py
Normal 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)}")
|
||||||
157
ressult/app/routes/partners.py
Normal file
157
ressult/app/routes/partners.py
Normal 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))
|
||||||
64
ressult/app/routes/sales.py
Normal file
64
ressult/app/routes/sales.py
Normal 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))
|
||||||
103
ressult/app/routes/upload.py
Normal file
103
ressult/app/routes/upload.py
Normal 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
24
ressult/config.json
Normal 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
196
ressult/database_init.py
Normal 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
9
ressult/gui/__init__.py
Normal 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
|
||||||
BIN
ressult/gui/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/login_window.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/login_window.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/main_window.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/main_window.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/material_calculator.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/material_calculator.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/partner_form.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/partner_form.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/sales_history.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/sales_history.cpython-314.pyc
Normal file
Binary file not shown.
253
ressult/gui/login_window.py
Normal file
253
ressult/gui/login_window.py
Normal 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
574
ressult/gui/main_window
Normal 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
574
ressult/gui/main_window.py
Normal 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"
|
||||||
|
"учета продаж и расчета бизнес-показателей."
|
||||||
|
)
|
||||||
616
ressult/gui/main_window.py.bak
Normal file
616
ressult/gui/main_window.py.bak
Normal 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"
|
||||||
|
"учета продаж и расчета бизнес-показателей."
|
||||||
|
)
|
||||||
160
ressult/gui/material_calculator.py
Normal file
160
ressult/gui/material_calculator.py
Normal 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
344
ressult/gui/orders_panel.py
Normal 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
193
ressult/gui/partner_form.py
Normal 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)}")
|
||||||
186
ressult/gui/partner_form.py.bak
Normal file
186
ressult/gui/partner_form.py.bak
Normal 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)}")
|
||||||
91
ressult/gui/sales_history.py
Normal file
91
ressult/gui/sales_history.py
Normal 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
11
ressult/requirements.txt
Normal 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
17
ressult/run.py
Normal 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
51
ressult/run_gui.py
Normal 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()
|
||||||
2
robbery/master_pol-module_1_2/.gitignore
vendored
Normal file
2
robbery/master_pol-module_1_2/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
**/__pycache__/
|
||||||
|
.venv/
|
||||||
3
robbery/master_pol-module_1_2/.idea/.gitignore
generated
vendored
Normal file
3
robbery/master_pol-module_1_2/.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
1
robbery/master_pol-module_1_2/.idea/.name
generated
Normal file
1
robbery/master_pol-module_1_2/.idea/.name
generated
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
main.py
|
||||||
6
robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
10
robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml
generated
Normal file
10
robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml
generated
Normal 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>
|
||||||
7
robbery/master_pol-module_1_2/.idea/misc.xml
generated
Normal file
7
robbery/master_pol-module_1_2/.idea/misc.xml
generated
Normal 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>
|
||||||
8
robbery/master_pol-module_1_2/.idea/modules.xml
generated
Normal file
8
robbery/master_pol-module_1_2/.idea/modules.xml
generated
Normal 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>
|
||||||
55
robbery/master_pol-module_1_2/README.md
Normal file
55
robbery/master_pol-module_1_2/README.md
Normal 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/` — ресурсы (цвета, шрифты)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -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)}"
|
||||||
|
)
|
||||||
94
robbery/master_pol-module_1_2/app/components/partner_card.py
Normal file
94
robbery/master_pol-module_1_2/app/components/partner_card.py
Normal 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)
|
||||||
|
)
|
||||||
84
robbery/master_pol-module_1_2/app/database/db.py
Normal file
84
robbery/master_pol-module_1_2/app/database/db.py
Normal 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}")
|
||||||
460
robbery/master_pol-module_1_2/app/database/script.sql
Normal file
460
robbery/master_pol-module_1_2/app/database/script.sql
Normal 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 ;
|
||||||
29
robbery/master_pol-module_1_2/app/dto/partners_dto.py
Normal file
29
robbery/master_pol-module_1_2/app/dto/partners_dto.py
Normal 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
|
||||||
11
robbery/master_pol-module_1_2/app/main.py
Normal file
11
robbery/master_pol-module_1_2/app/main.py
Normal 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()
|
||||||
94
robbery/master_pol-module_1_2/app/pages/auth_page.py
Normal file
94
robbery/master_pol-module_1_2/app/pages/auth_page.py
Normal 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,
|
||||||
|
}
|
||||||
|
)
|
||||||
130
robbery/master_pol-module_1_2/app/pages/partners_page.py
Normal file
130
robbery/master_pol-module_1_2/app/pages/partners_page.py
Normal 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()
|
||||||
4
robbery/master_pol-module_1_2/app/res/colors.py
Normal file
4
robbery/master_pol-module_1_2/app/res/colors.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
MAIN_COLOR = "#FFFFFF"
|
||||||
|
SECONDARY_COLOR = "#F4E8D3"
|
||||||
|
ACCENT_COLOR = "#67BA80"
|
||||||
|
ACCENT_COLOR_HOVER = "#529265"
|
||||||
1
robbery/master_pol-module_1_2/app/res/fonts.py
Normal file
1
robbery/master_pol-module_1_2/app/res/fonts.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
MAIN_FONT = "Segoe UI"
|
||||||
BIN
robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico
Normal file
BIN
robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
robbery/master_pol-module_1_2/app/res/imgs/master_pol.png
Normal file
BIN
robbery/master_pol-module_1_2/app/res/imgs/master_pol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
35
robbery/master_pol-module_1_2/app/res/styles.py
Normal file
35
robbery/master_pol-module_1_2/app/res/styles.py
Normal 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,
|
||||||
|
)
|
||||||
BIN
robbery/master_pol-module_1_2/requirements.txt
Normal file
BIN
robbery/master_pol-module_1_2/requirements.txt
Normal file
Binary file not shown.
Binary file not shown.
BIN
service_requests_v2.db
Normal file
BIN
service_requests_v2.db
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue