After Graduate Update

This commit is contained in:
Daniel 2025-11-26 19:31:33 +03:00
parent b92a91ab37
commit c6917dd85e
69 changed files with 7540 additions and 0 deletions

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

@ -0,0 +1,9 @@
# gui/__init__.py
"""
Пакет графического интерфейса с авторизацией
"""
from .login_window import LoginWindow
from .main_window import MainWindow
from .partner_form import PartnerForm
from .sales_history import SalesHistoryWindow
from .material_calculator import MaterialCalculatorWindow

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

@ -0,0 +1,253 @@
# gui/login_window.py
"""
Окно авторизации менеджера
Соответствует требованиям ТЗ по аутентификации
"""
import sys
from PyQt6.QtWidgets import (QApplication, QDialog, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QMessageBox,
QFrame, QCheckBox)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont, QPixmap, QIcon
import requests
from requests.auth import HTTPBasicAuth
class LoginWindow(QDialog):
"""Окно авторизации системы MasterPol"""
login_success = pyqtSignal(dict) # Сигнал об успешной авторизации
def __init__(self):
super().__init__()
self.setup_ui()
self.load_settings()
def setup_ui(self):
"""Настройка интерфейса окна авторизации"""
self.setWindowTitle("MasterPol - Авторизация")
self.setFixedSize(400, 500)
self.setModal(True)
# Установка иконки приложения
try:
self.setWindowIcon(QIcon("resources/icon.png"))
except:
pass
layout = QVBoxLayout()
layout.setContentsMargins(30, 30, 30, 30)
layout.setSpacing(0)
# Заголовок
title_label = QLabel("MasterPol")
title_label.setFont(QFont("Arial", 24, QFont.Weight.Bold))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("color: #007acc; margin-bottom: 20px;")
subtitle_label = QLabel("Система управления партнерами")
subtitle_label.setFont(QFont("Arial", 12))
subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
subtitle_label.setStyleSheet("color: #666; margin-bottom: 30px;")
layout.addWidget(title_label)
layout.addWidget(subtitle_label)
# Форма авторизаци
form_frame = QFrame()
form_frame.setStyleSheet("""
QFrame {
background-color: white;
border: 0px solid #ddd;
border-radius: 4px;
padding: 20px;
}
""")
form_layout = QVBoxLayout()
form_layout.setSpacing(15)
# Поле логина
username_layout = QVBoxLayout()
username_label = QLabel("Имя пользователя:")
username_label.setStyleSheet("font-weight: bold; color: #333;")
self.username_input = QLineEdit()
self.username_input.setPlaceholderText("Введите имя пользователя")
self.username_input.setStyleSheet("""
QLineEdit {
padding: 8px 12px;
border: 2px solid #ccc;
border-radius: 6px;
font-size: 14px;
}
QLineEdit:focus {
border-color: #007acc;
}
""")
username_layout.addWidget(username_label)
username_layout.addWidget(self.username_input)
# Поле пароля
password_layout = QVBoxLayout()
password_label = QLabel("Пароль:")
password_label.setStyleSheet("font-weight: bold; color: #333;")
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("Введите пароль")
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
self.password_input.setStyleSheet("""
QLineEdit {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
QLineEdit:focus {
border-color: #007acc;
}
""")
password_layout.addWidget(password_label)
password_layout.addWidget(self.password_input)
# Запомнить меня
self.remember_checkbox = QCheckBox("Запомнить меня")
self.remember_checkbox.setStyleSheet("color: #333;")
# Кнопка входа
self.login_button = QPushButton("Войти в систему")
self.login_button.clicked.connect(self.authenticate)
self.login_button.setStyleSheet("""
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 12px;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #005a9e;
}
QPushButton:disabled {
background-color: #ccc;
color: #666;
}
""")
# Подсказка
hint_label = QLabel("Используйте логин: manager, пароль: pass123")
hint_label.setStyleSheet("color: #666; font-size: 12px; margin-top: 10px;")
hint_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
form_layout.addLayout(username_layout)
form_layout.addLayout(password_layout)
form_layout.addWidget(self.remember_checkbox)
form_layout.addWidget(self.login_button)
form_layout.addWidget(hint_label)
form_frame.setLayout(form_layout)
layout.addWidget(form_frame)
# Информация о системе
info_label = QLabel("MasterPol v1.0.0\nСистема управления партнерами и продажами")
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
info_label.setStyleSheet("color: #999; font-size: 11px; margin-top: 20px;")
layout.addWidget(info_label)
self.setLayout(layout)
# Подключаем обработчики событий
self.username_input.returnPressed.connect(self.authenticate)
self.password_input.returnPressed.connect(self.authenticate)
def load_settings(self):
"""Загрузка сохраненных настроек авторизации"""
try:
# Здесь можно добавить загрузку из файла настроек
# Пока просто устанавливаем значения по умолчанию
self.username_input.setText("manager")
except:
pass
def save_settings(self):
"""Сохранение настроек авторизации"""
if self.remember_checkbox.isChecked():
# Здесь можно добавить сохранение в файл настроек
pass
def authenticate(self):
"""Аутентификация пользователя"""
username = self.username_input.text().strip()
password = self.password_input.text().strip()
if not username or not password:
QMessageBox.warning(self, "Ошибка", "Заполните все поля")
return
# Блокируем кнопку во время аутентификации
self.login_button.setEnabled(False)
self.login_button.setText("Проверка...")
try:
# Выполняем аутентификацию через API
response = requests.post(
"http://localhost:8000/api/v1/auth/login",
auth=HTTPBasicAuth(username, password),
timeout=10
)
if response.status_code == 200:
user_data = response.json()
# Сохраняем настройки
self.save_settings()
# Сохраняем учетные данные для будущих запросов
user_data['auth'] = HTTPBasicAuth(username, password)
# Отправляем сигнал об успешной авторизации
self.login_success.emit(user_data)
else:
QMessageBox.warning(
self,
"Ошибка авторизации",
"Неверное имя пользователя или пароль"
)
except requests.exceptions.ConnectionError:
QMessageBox.critical(
self,
"Ошибка подключения",
"Не удалось подключиться к серверу.\n"
"Убедитесь, что сервер запущен на localhost:8000"
)
except requests.exceptions.Timeout:
QMessageBox.critical(
self,
"Ошибка подключения",
"Превышено время ожидания ответа от сервера"
)
except Exception as e:
QMessageBox.critical(
self,
"Ошибка",
f"Произошла непредвиденная ошибка:\n{str(e)}"
)
finally:
# Разблокируем кнопку
self.login_button.setEnabled(True)
self.login_button.setText("Войти в систему")
def main():
"""Точка входа для тестирования окна авторизации"""
app = QApplication(sys.argv)
window = LoginWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

574
ressult/gui/main_window Normal file
View file

@ -0,0 +1,574 @@
# gui/main_window.py
"""
Главное окно приложения PyQt6 с поддержкой авторизации
"""
import sys
import requests
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QListWidget,
QListWidgetItem, QMessageBox, QFrame, QStackedWidget,
QMenuBar, QMenu, QStatusBar, QToolBar)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction
from .partner_form import PartnerForm
from .sales_history import SalesHistoryWindow
from .material_calculator import MaterialCalculatorWindow
class PartnerCard(QFrame):
"""Карточка партнера для отображения в списке"""
partner_clicked = pyqtSignal(dict)
def __init__(self, partner_data):
super().__init__()
self.partner_data = partner_data
self.setup_ui()
def setup_ui(self):
self.setFrameStyle(QFrame.Shape.StyledPanel)
self.setStyleSheet("""
PartnerCard {
background-color: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
margin: 4px;
}
PartnerCard:hover {
background-color: #f5f5f5;
border-color: #007acc;
}
""")
layout = QVBoxLayout()
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(4)
# Заголовок с типом и названием
header_layout = QHBoxLayout()
header_layout.setSpacing(4)
type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |")
type_label.setStyleSheet("color: #666; font-weight: bold;")
name_label = QLabel(self.partner_data['company_name'])
name_label.setStyleSheet("font-weight: bold; font-size: 14px;")
name_label.setWordWrap(True)
# Безопасное преобразование рейтинга
rating_value = self.partner_data.get('rating', 0)
if isinstance(rating_value, float):
rating_value = int(rating_value)
rating_label = QLabel(f"{rating_value}%")
rating_label.setStyleSheet("color: #007acc; font-weight: bold;")
header_layout.addWidget(type_label)
header_layout.addWidget(name_label)
header_layout.addStretch()
header_layout.addWidget(rating_label)
# Информация о директоре
director_label = QLabel(self.partner_data.get('director_name', 'Директор не указан'))
director_label.setStyleSheet("color: #444;")
# Контактная информация
phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан'))
phone_label.setStyleSheet("color: #666;")
layout.addLayout(header_layout)
layout.addWidget(director_label)
layout.addWidget(phone_label)
self.setLayout(layout)
def mousePressEvent(self, event):
"""Обработка клика на карточке"""
if event.button() == Qt.MouseButton.LeftButton:
self.partner_clicked.emit(self.partner_data)
class MainWindow(QMainWindow):
"""Главное окно приложения с поддержкой авторизации"""
def __init__(self, user_data):
super().__init__()
self.user_data = user_data
self.current_partner = None
self.auth = user_data.get('auth')
self.setup_ui()
self.load_partners()
def setup_ui(self):
"""Настройка интерфейса главного окна"""
self.setWindowTitle(f"MasterPol - Система управления партнерами")
self.setGeometry(100, 100, 1200, 700)
# Установка иконки приложения
try:
self.setWindowIcon(QIcon("resources/icon.png"))
except:
pass
# Создание меню
self.create_menu()
# Создание тулбара
self.create_toolbar()
# Создание статусной строки
self.create_statusbar()
# Центральный виджет
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
# Левая панель - список партнеров
left_panel = self.create_partners_panel()
main_layout.addWidget(left_panel, 1)
# Правая панель - детальная информация
self.right_panel = self.create_details_panel()
main_layout.addWidget(self.right_panel, 2)
central_widget.setLayout(main_layout)
def create_menu(self):
"""Создание меню приложения"""
menubar = self.menuBar()
# Меню Файл
file_menu = menubar.addMenu('Файл')
refresh_action = QAction('Обновить', self)
refresh_action.setShortcut('F5')
refresh_action.triggered.connect(self.load_partners)
file_menu.addAction(refresh_action)
file_menu.addSeparator()
logout_action = QAction('Выход', self)
logout_action.setShortcut('Ctrl+Q')
logout_action.triggered.connect(self.logout)
file_menu.addAction(logout_action)
# Меню Сервис
service_menu = menubar.addMenu('Сервис')
calc_action = QAction('Калькулятор материалов', self)
calc_action.triggered.connect(self.show_material_calculator)
service_menu.addAction(calc_action)
# Меню Справка
help_menu = menubar.addMenu('Справка')
about_action = QAction('О программе', self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def create_toolbar(self):
"""Создание панели инструментов"""
toolbar = QToolBar("Основные инструменты")
self.addToolBar(toolbar)
refresh_action = QAction('Обновить', self)
refresh_action.triggered.connect(self.load_partners)
toolbar.addAction(refresh_action)
toolbar.addSeparator()
add_partner_action = QAction('Добавить партнера', self)
add_partner_action.triggered.connect(self.show_add_partner_form)
toolbar.addAction(add_partner_action)
def create_statusbar(self):
"""Создание статусной строки"""
statusbar = self.statusBar()
user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}"
statusbar.showMessage(user_info)
def create_partners_panel(self):
"""Создание панели списка партнеров"""
panel = QWidget()
panel.setMaximumWidth(400)
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Заголовок
title = QLabel("Партнеры")
title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("padding: 10px;")
layout.addWidget(title)
# Панель управления
control_layout = QHBoxLayout()
control_layout.setSpacing(10)
self.add_button = QPushButton("Добавить партнера")
self.add_button.clicked.connect(self.show_add_partner_form)
self.add_button.setStyleSheet("""
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.refresh_button = QPushButton("Обновить")
self.refresh_button.clicked.connect(self.load_partners)
self.refresh_button.setStyleSheet("""
QPushButton {
background-color: #6c757d;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #545b62;
}
""")
control_layout.addWidget(self.add_button)
control_layout.addWidget(self.refresh_button)
control_layout.addStretch()
layout.addLayout(control_layout)
# Список партнеров
self.partners_list = QListWidget()
self.partners_list.setStyleSheet("""
QListWidget {
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
outline: none;
}
QListWidget::item {
border: none;
padding: 0px;
}
QListWidget::item:selected {
background-color: transparent;
}
""")
layout.addWidget(self.partners_list)
# Кнопка расчета материалов
self.calc_button = QPushButton("Калькулятор материалов")
self.calc_button.clicked.connect(self.show_material_calculator)
self.calc_button.setStyleSheet("""
QPushButton {
background-color: #17a2b8;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #138496;
}
""")
layout.addWidget(self.calc_button)
panel.setLayout(layout)
return panel
def create_details_panel(self):
"""Создание панели детальной информации"""
panel = QWidget()
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Заголовок детальной информации
self.details_title = QLabel("Выберите партнера")
self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold))
self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.details_title.setStyleSheet("padding: 10px;")
layout.addWidget(self.details_title)
# Детальная информация о партнере - создаем пустой frame
self.details_frame = QFrame()
self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
self.details_frame.setStyleSheet("""
QFrame {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
}
""")
self.details_layout = QVBoxLayout()
self.details_layout.setSpacing(8)
self.details_frame.setLayout(self.details_layout)
self.details_frame.hide()
layout.addWidget(self.details_frame)
# Кнопки управления выбранным партнером
self.control_buttons = QWidget()
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(10)
self.edit_button = QPushButton("Редактировать")
self.edit_button.clicked.connect(self.edit_partner)
self.edit_button.setStyleSheet("""
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.edit_button.hide()
self.sales_button = QPushButton("История продаж")
self.sales_button.clicked.connect(self.show_sales_history)
self.sales_button.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #218838;
}
""")
self.sales_button.hide()
self.discount_button = QPushButton("Расчет скидки")
self.discount_button.clicked.connect(self.calculate_discount)
self.discount_button.setStyleSheet("""
QPushButton {
background-color: #ffc107;
color: black;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #e0a800;
}
""")
self.discount_button.hide()
buttons_layout.addWidget(self.edit_button)
buttons_layout.addWidget(self.sales_button)
buttons_layout.addWidget(self.discount_button)
buttons_layout.addStretch()
self.control_buttons.setLayout(buttons_layout)
layout.addWidget(self.control_buttons)
# Добавляем растягивающийся элемент в конец
layout.addStretch()
panel.setLayout(layout)
return panel
def load_partners(self):
"""Загрузка списка партнеров из API с авторизацией"""
try:
response = requests.get(
"http://localhost:8000/api/v1/partners",
auth=self.auth,
timeout=10
)
if response.status_code == 200:
self.partners_list.clear()
partners = response.json()
for partner in partners:
item = QListWidgetItem()
card = PartnerCard(partner)
card.partner_clicked.connect(self.show_partner_details)
# Устанавливаем фиксированный размер для элемента
item.setSizeHint(card.sizeHint())
self.partners_list.addItem(item)
self.partners_list.setItemWidget(item, card)
# Сбрасываем выделение
self.partners_list.clearSelection()
self.current_partner = None
self.details_title.setText("Выберите партнера")
self.details_frame.hide()
self.edit_button.hide()
self.sales_button.hide()
self.discount_button.hide()
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
self.logout()
else:
QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров")
except requests.exceptions.ConnectionError:
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}")
def show_partner_details(self, partner_data):
"""Отображение детальной информации о партнере"""
self.current_partner = partner_data
self.details_title.setText(partner_data['company_name'])
# Создаем новый виджет для деталей вместо очистки layout
new_details_frame = QFrame()
new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
new_details_frame.setStyleSheet("""
QFrame {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
}
""")
new_details_layout = QVBoxLayout()
new_details_layout.setSpacing(8)
# Добавляем новую информацию
details = [
("Тип:", partner_data.get('partner_type', 'Не указан')),
("ИНН:", partner_data.get('inn', 'Не указан')),
("Директор:", partner_data.get('director_name', 'Не указан')),
("Телефон:", partner_data.get('phone', 'Не указан')),
("Email:", partner_data.get('email', 'Не указан')),
("Рейтинг:", str(partner_data.get('rating', 0))),
("Адрес:", partner_data.get('legal_address', 'Не указан')),
("Регионы:", partner_data.get('sales_locations', 'Не указан'))
]
for label, value in details:
row_widget = QWidget()
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(0, 2, 0, 2)
label_widget = QLabel(label)
label_widget.setStyleSheet("font-weight: bold; min-width: 100px;")
label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
value_widget = QLabel(str(value))
value_widget.setWordWrap(True)
value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
row_layout.addWidget(label_widget)
row_layout.addWidget(value_widget)
row_layout.addStretch()
new_details_layout.addWidget(row_widget)
new_details_frame.setLayout(new_details_layout)
# Заменяем старый details_frame на новый
old_frame = self.details_frame
layout = self.right_panel.layout()
layout.replaceWidget(old_frame, new_details_frame)
old_frame.deleteLater()
self.details_frame = new_details_frame
self.details_layout = new_details_layout
self.details_frame.show()
self.edit_button.show()
self.sales_button.show()
self.discount_button.show()
def show_add_partner_form(self):
"""Открытие формы добавления партнера"""
form = PartnerForm(self, auth=self.auth)
form.partner_saved.connect(self.load_partners)
form.exec()
def edit_partner(self):
"""Редактирование выбранного партнера"""
if self.current_partner:
form = PartnerForm(self, self.current_partner, auth=self.auth)
form.partner_saved.connect(self.load_partners)
form.exec()
def show_sales_history(self):
"""Открытие истории продаж партнера"""
if self.current_partner:
sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth)
sales_window.exec()
def calculate_discount(self):
"""Расчет скидки для партнера с авторизацией"""
if self.current_partner:
try:
response = requests.get(
f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount",
auth=self.auth,
timeout=10
)
if response.status_code == 200:
discount_data = response.json()
QMessageBox.information(
self,
"Расчет скидки",
f"Партнер: {self.current_partner['company_name']}\n"
f"Общие продажи: {discount_data['total_sales']}\n"
f"Скидка: {discount_data['discount_percent']}%"
)
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
self.logout()
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}")
def show_material_calculator(self):
"""Открытие калькулятора материалов"""
calculator = MaterialCalculatorWindow(self, auth=self.auth)
calculator.exec()
def logout(self):
"""Выход из системы"""
reply = QMessageBox.question(
self,
"Подтверждение выхода",
"Вы уверены, что хотите выйти из системы?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.close()
# Здесь можно добавить вызов окна авторизации
# или перезапуск приложения
def show_about(self):
"""Показать информацию о программе"""
QMessageBox.about(
self,
"О программе MasterPol",
"MasterPol - Система управления партнерами\n\n"
"Версия: 1.0.0\n"
"Разработчик: Команда MasterPol\n\n"
"Система предназначена для управления партнерами,\n"
"учета продаж и расчета бизнес-показателей."
)

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

@ -0,0 +1,574 @@
# gui/main_window.py
"""
Главное окно приложения PyQt6 с поддержкой авторизации
"""
import sys
import requests
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QListWidget,
QListWidgetItem, QMessageBox, QFrame, QStackedWidget,
QMenuBar, QMenu, QStatusBar, QToolBar)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction
from .partner_form import PartnerForm
from .sales_history import SalesHistoryWindow
from .material_calculator import MaterialCalculatorWindow
class PartnerCard(QFrame):
"""Карточка партнера для отображения в списке"""
partner_clicked = pyqtSignal(dict)
def __init__(self, partner_data):
super().__init__()
self.partner_data = partner_data
self.setup_ui()
def setup_ui(self):
self.setFrameStyle(QFrame.Shape.StyledPanel)
self.setStyleSheet("""
PartnerCard {
background-color: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
margin: 4px;
}
PartnerCard:hover {
background-color: #f5f5f5;
border-color: #007acc;
}
""")
layout = QVBoxLayout()
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(4)
# Заголовок с типом и названием
header_layout = QHBoxLayout()
header_layout.setSpacing(4)
type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |")
type_label.setStyleSheet("color: #666; font-weight: bold;")
name_label = QLabel(self.partner_data['company_name'])
name_label.setStyleSheet("font-weight: bold; font-size: 14px;")
name_label.setWordWrap(True)
# Безопасное преобразование рейтинга
rating_value = self.partner_data.get('rating', 0)
if isinstance(rating_value, float):
rating_value = int(rating_value)
rating_label = QLabel(f"{rating_value}%")
rating_label.setStyleSheet("color: #007acc; font-weight: bold;")
header_layout.addWidget(type_label)
header_layout.addWidget(name_label)
header_layout.addStretch()
header_layout.addWidget(rating_label)
# Информация о директоре
director_label = QLabel(self.partner_data.get('director_name', 'Директор не указан'))
director_label.setStyleSheet("color: #444;")
# Контактная информация
phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан'))
phone_label.setStyleSheet("color: #666;")
layout.addLayout(header_layout)
layout.addWidget(director_label)
layout.addWidget(phone_label)
self.setLayout(layout)
def mousePressEvent(self, event):
"""Обработка клика на карточке"""
if event.button() == Qt.MouseButton.LeftButton:
self.partner_clicked.emit(self.partner_data)
class MainWindow(QMainWindow):
"""Главное окно приложения с поддержкой авторизации"""
def __init__(self, user_data):
super().__init__()
self.user_data = user_data
self.current_partner = None
self.auth = user_data.get('auth')
self.setup_ui()
self.load_partners()
def setup_ui(self):
"""Настройка интерфейса главного окна"""
self.setWindowTitle(f"MasterPol - Система управления партнерами")
self.setGeometry(100, 100, 1200, 700)
# Установка иконки приложения
try:
self.setWindowIcon(QIcon("resources/icon.png"))
except:
pass
# Создание меню
self.create_menu()
# Создание тулбара
self.create_toolbar()
# Создание статусной строки
self.create_statusbar()
# Центральный виджет
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
# Левая панель - список партнеров
left_panel = self.create_partners_panel()
main_layout.addWidget(left_panel, 1)
# Правая панель - детальная информация
self.right_panel = self.create_details_panel()
main_layout.addWidget(self.right_panel, 2)
central_widget.setLayout(main_layout)
def create_menu(self):
"""Создание меню приложения"""
menubar = self.menuBar()
# Меню Файл
file_menu = menubar.addMenu('Файл')
refresh_action = QAction('Обновить', self)
refresh_action.setShortcut('F5')
refresh_action.triggered.connect(self.load_partners)
file_menu.addAction(refresh_action)
file_menu.addSeparator()
logout_action = QAction('Выход', self)
logout_action.setShortcut('Ctrl+Q')
logout_action.triggered.connect(self.logout)
file_menu.addAction(logout_action)
# Меню Сервис
service_menu = menubar.addMenu('Сервис')
calc_action = QAction('Калькулятор материалов', self)
calc_action.triggered.connect(self.show_material_calculator)
service_menu.addAction(calc_action)
# Меню Справка
help_menu = menubar.addMenu('Справка')
about_action = QAction('О программе', self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def create_toolbar(self):
"""Создание панели инструментов"""
toolbar = QToolBar("Основные инструменты")
self.addToolBar(toolbar)
refresh_action = QAction('Обновить', self)
refresh_action.triggered.connect(self.load_partners)
toolbar.addAction(refresh_action)
toolbar.addSeparator()
add_partner_action = QAction('Добавить партнера', self)
add_partner_action.triggered.connect(self.show_add_partner_form)
toolbar.addAction(add_partner_action)
def create_statusbar(self):
"""Создание статусной строки"""
statusbar = self.statusBar()
user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}"
statusbar.showMessage(user_info)
def create_partners_panel(self):
"""Создание панели списка партнеров"""
panel = QWidget()
panel.setMaximumWidth(400)
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Заголовок
title = QLabel("Партнеры")
title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("padding: 10px;")
layout.addWidget(title)
# Панель управления
control_layout = QHBoxLayout()
control_layout.setSpacing(10)
self.add_button = QPushButton("Добавить партнера")
self.add_button.clicked.connect(self.show_add_partner_form)
self.add_button.setStyleSheet("""
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.refresh_button = QPushButton("Обновить")
self.refresh_button.clicked.connect(self.load_partners)
self.refresh_button.setStyleSheet("""
QPushButton {
background-color: #6c757d;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #545b62;
}
""")
control_layout.addWidget(self.add_button)
control_layout.addWidget(self.refresh_button)
control_layout.addStretch()
layout.addLayout(control_layout)
# Список партнеров
self.partners_list = QListWidget()
self.partners_list.setStyleSheet("""
QListWidget {
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
outline: none;
}
QListWidget::item {
border: none;
padding: 0px;
}
QListWidget::item:selected {
background-color: transparent;
}
""")
layout.addWidget(self.partners_list)
# Кнопка расчета материалов
self.calc_button = QPushButton("Калькулятор материалов")
self.calc_button.clicked.connect(self.show_material_calculator)
self.calc_button.setStyleSheet("""
QPushButton {
background-color: #17a2b8;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #138496;
}
""")
layout.addWidget(self.calc_button)
panel.setLayout(layout)
return panel
def create_details_panel(self):
"""Создание панели детальной информации"""
panel = QWidget()
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Заголовок детальной информации
self.details_title = QLabel("Выберите партнера")
self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold))
self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.details_title.setStyleSheet("padding: 10px;")
layout.addWidget(self.details_title)
# Детальная информация о партнере - создаем пустой frame
self.details_frame = QFrame()
self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
self.details_frame.setStyleSheet("""
QFrame {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
}
""")
self.details_layout = QVBoxLayout()
self.details_layout.setSpacing(8)
self.details_frame.setLayout(self.details_layout)
self.details_frame.hide()
layout.addWidget(self.details_frame)
# Кнопки управления выбранным партнером
self.control_buttons = QWidget()
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(10)
self.edit_button = QPushButton("Редактировать")
self.edit_button.clicked.connect(self.edit_partner)
self.edit_button.setStyleSheet("""
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.edit_button.hide()
self.sales_button = QPushButton("История продаж")
self.sales_button.clicked.connect(self.show_sales_history)
self.sales_button.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #218838;
}
""")
self.sales_button.hide()
self.discount_button = QPushButton("Расчет скидки")
self.discount_button.clicked.connect(self.calculate_discount)
self.discount_button.setStyleSheet("""
QPushButton {
background-color: #ffc107;
color: black;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #e0a800;
}
""")
self.discount_button.hide()
buttons_layout.addWidget(self.edit_button)
buttons_layout.addWidget(self.sales_button)
buttons_layout.addWidget(self.discount_button)
buttons_layout.addStretch()
self.control_buttons.setLayout(buttons_layout)
layout.addWidget(self.control_buttons)
# Добавляем растягивающийся элемент в конец
layout.addStretch()
panel.setLayout(layout)
return panel
def load_partners(self):
"""Загрузка списка партнеров из API с авторизацией"""
try:
response = requests.get(
"http://localhost:8000/api/v1/partners",
auth=self.auth,
timeout=10
)
if response.status_code == 200:
self.partners_list.clear()
partners = response.json()
for partner in partners:
item = QListWidgetItem()
card = PartnerCard(partner)
card.partner_clicked.connect(self.show_partner_details)
# Устанавливаем фиксированный размер для элемента
item.setSizeHint(card.sizeHint())
self.partners_list.addItem(item)
self.partners_list.setItemWidget(item, card)
# Сбрасываем выделение
self.partners_list.clearSelection()
self.current_partner = None
self.details_title.setText("Выберите партнера")
self.details_frame.hide()
self.edit_button.hide()
self.sales_button.hide()
self.discount_button.hide()
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
self.logout()
else:
QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров")
except requests.exceptions.ConnectionError:
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}")
def show_partner_details(self, partner_data):
"""Отображение детальной информации о партнере"""
self.current_partner = partner_data
self.details_title.setText(partner_data['company_name'])
# Создаем новый виджет для деталей вместо очистки layout
new_details_frame = QFrame()
new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
new_details_frame.setStyleSheet("""
QFrame {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
}
""")
new_details_layout = QVBoxLayout()
new_details_layout.setSpacing(8)
# Добавляем новую информацию
details = [
("Тип:", partner_data.get('partner_type', 'Не указан')),
("ИНН:", partner_data.get('inn', 'Не указан')),
("Директор:", partner_data.get('director_name', 'Не указан')),
("Телефон:", partner_data.get('phone', 'Не указан')),
("Email:", partner_data.get('email', 'Не указан')),
("Рейтинг:", str(partner_data.get('rating', 0))),
("Адрес:", partner_data.get('legal_address', 'Не указан')),
("Регионы:", partner_data.get('sales_locations', 'Не указан'))
]
for label, value in details:
row_widget = QWidget()
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(0, 2, 0, 2)
label_widget = QLabel(label)
label_widget.setStyleSheet("font-weight: bold; min-width: 100px;")
label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
value_widget = QLabel(str(value))
value_widget.setWordWrap(True)
value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
row_layout.addWidget(label_widget)
row_layout.addWidget(value_widget)
row_layout.addStretch()
new_details_layout.addWidget(row_widget)
new_details_frame.setLayout(new_details_layout)
# Заменяем старый details_frame на новый
old_frame = self.details_frame
layout = self.right_panel.layout()
layout.replaceWidget(old_frame, new_details_frame)
old_frame.deleteLater()
self.details_frame = new_details_frame
self.details_layout = new_details_layout
self.details_frame.show()
self.edit_button.show()
self.sales_button.show()
self.discount_button.show()
def show_add_partner_form(self):
"""Открытие формы добавления партнера"""
form = PartnerForm(self, auth=self.auth)
form.partner_saved.connect(self.load_partners)
form.exec()
def edit_partner(self):
"""Редактирование выбранного партнера"""
if self.current_partner:
form = PartnerForm(self, self.current_partner, auth=self.auth)
form.partner_saved.connect(self.load_partners)
form.exec()
def show_sales_history(self):
"""Открытие истории продаж партнера"""
if self.current_partner:
sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth)
sales_window.exec()
def calculate_discount(self):
"""Расчет скидки для партнера с авторизацией"""
if self.current_partner:
try:
response = requests.get(
f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount",
auth=self.auth,
timeout=10
)
if response.status_code == 200:
discount_data = response.json()
QMessageBox.information(
self,
"Расчет скидки",
f"Партнер: {self.current_partner['company_name']}\n"
f"Общие продажи: {discount_data['total_sales']}\n"
f"Скидка: {discount_data['discount_percent']}%"
)
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
self.logout()
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}")
def show_material_calculator(self):
"""Открытие калькулятора материалов"""
calculator = MaterialCalculatorWindow(self, auth=self.auth)
calculator.exec()
def logout(self):
"""Выход из системы"""
reply = QMessageBox.question(
self,
"Подтверждение выхода",
"Вы уверены, что хотите выйти из системы?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.close()
# Здесь можно добавить вызов окна авторизации
# или перезапуск приложения
def show_about(self):
"""Показать информацию о программе"""
QMessageBox.about(
self,
"О программе MasterPol",
"MasterPol - Система управления партнерами\n\n"
"Версия: 1.0.0\n"
"Разработчик: Команда MasterPol\n\n"
"Система предназначена для управления партнерами,\n"
"учета продаж и расчета бизнес-показателей."
)

View file

@ -0,0 +1,616 @@
# gui/main_wind/w.py
"""
Главное окно приложения PyQt6 с поддержкой авторизации
"""
import sys
import requests
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QListWidget,
QListWidgetItem, QMessageBox, QFrame, QStackedWidget,
QMenuBar, QMenu, QStatusBar, QToolBar)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction
from .partner_form import PartnerForm
from .sales_history import SalesHistoryWindow
from .material_calculator import MaterialCalculatorWindow
class PartnerCard(QFrame):
"""Карточка партнера для отображения в списке"""
partner_clicked = pyqtSignal(dict)
def __init__(self, partner_data):
super().__init__()
self.partner_data = partner_data
self.setup_ui()
def setup_ui(self):
self.setFrameStyle(QFrame.Shape.StyledPanel)
self.setStyleSheet("""
PartnerCard {
background-color: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
margin: 4px;
}
PartnerCard:hover {
background-color: #f5f5f5;
border-color: #007acc;
}
""")
layout = QVBoxLayout()
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(4)
# Заголовок с типом и названием
header_layout = QHBoxLayout()
header_layout.setSpacing(4)
type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |")
type_label.setStyleSheet("color: #666; font-weight: bold;")
name_label = QLabel(self.partner_data['company_name'])
name_label.setStyleSheet("font-weight: bold; font-size: 14px;")
name_label.setWordWrap(True)
# Безопасное преобразование рейтинга
rating_value = self.partner_data.get('rating', 0)
if isinstance(rating_value, float):
rating_value = int(rating_value)
rating_label = QLabel(f"{rating_value}%")
rating_label.setStyleSheet("color: #007acc; font-weight: bold;")
header_layout.addWidget(type_label)
header_layout.addWidget(name_label)
header_layout.addStretch()
header_layout.addWidget(rating_label)
# Информация о директоре
QLabel(self.partner_data.get('director_name', 'Директор не указан'))
director_label.setStyleSheet("color: #444;")
# Контактная информация
phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан'))
phone_label.setStyleSheet("color: #666;")
layout.addLayout(header_layout)
layout.addWidget(director_label)
layout.addWidget(phone_label)
self.setLayout(layout)
def mousePressEvent(self, event):
"""Обработка клика на карточке"""
if event.button() == Qt.MouseButton.LeftButton:
self.partner_clicked.emit(self.partner_data)
class MainWindow(QMainWindow):
"""Главное окно приложения с поддержкой авторизации"""
def __init__(self, user_data):
super().__init__()
self.user_data = user_data
self.current_partner = None
self.orders_panel = None
self.auth = user_data.get('auth')
self.setup_ui()
self.load_partners()
def setup_ui(self):
"""Настройка интерфейса главного окна"""
self.setWindowTitle(f"MasterPol - Система управления партнерами")
self.setGeometry(100, 100, 1200, 700)
# Установка иконки приложения
try:
self.setWindowIcon(QIcon("resources/icon.png"))
except:
pass
# Создание меню
self.create_menu()
# Создание тулбара
self.create_toolbar()
# Создание статусной строки
self.create_statusbar()
# Центральный виджет
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
# Левая панель - список партнеров
left_panel = self.create_partners_panel()
main_layout.addWidget(left_panel, 1)
# Правая панель - детальная информация
self.right_panel = self.create_details_panel()
main_layout.addWidget(self.right_panel, 2)
central_widget.setLayout(main_layout)
def create_menu(self):
"""Создание меню приложения"""
menubar = self.menuBar()
# Меню Файл
file_menu = menubar.addMenu('Файл')
refresh_action = QAction('Обновить', self)
refresh_action.setShortcut('F5')
refresh_action.triggered.connect(self.load_partners)
file_menu.addAction(refresh_action)
file_menu.addSeparator()
logout_action = QAction('Выход', self)
logout_action.setShortcut('Ctrl+Q')
logout_action.triggered.connect(self.logout)
file_menu.addAction(logout_action)
# Меню Сервис
service_menu = menubar.addMenu('Сервис')
calc_action = QAction('Калькулятор материалов', self)
calc_action.triggered.connect(self.show_material_calculator)
service_menu.addAction(calc_action)
# Меню Справка
help_menu = menubar.addMenu('Справка')
about_action = QAction('О программе', self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def create_toolbar(self):
"""Создание панели инструментов"""
toolbar = QToolBar("Основные инструменты")
self.addToolBar(toolbar)
refresh_action = QAction('Обновить', self)
refresh_action.triggered.connect(self.load_partners)
toolbar.addAction(refresh_action)
toolbar.addSeparator()
add_partner_action = QAction('Добавить партнера', self)
add_partner_action.triggered.connect(self.show_add_partner_form)
toolbar.addAction(add_partner_action)
def create_statusbar(self):
"""Создание статусной строки"""
statusbar = self.statusBar()
user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}"
statusbar.showMessage(user_info)
def create_partners_panel(self):
"""Создание панели списка партнеров"""
panel = QWidget()
panel.setMaximumWidth(400)
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Заголовок
title = QLabel("Партнеры")
title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("padding: 10px;")
layout.addWidget(title)
# Панель управления
control_layout = QHBoxLayout()
control_layout.setSpacing(10)
self.add_button = QPushButton("Добавить партнера")
self.add_button.clicked.connect(self.show_add_partner_form)
self.add_button.setStyleSheet("""
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.refresh_button = QPushButton("Обновить")
self.refresh_button.clicked.connect(self.load_partners)
self.refresh_button.setStyleSheet("""
QPushButton {
background-color: #6c757d;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #545b62;
}
""")
control_layout.addWidget(self.add_button)
control_layout.addWidget(self.refresh_button)
control_layout.addStretch()
layout.addLayout(control_layout)
# Список партнеров
self.partners_list = QListWidget()
self.partners_list.setStyleSheet("""
QListWidget {
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
outline: none;
}
QListWidget::item {
border: none;
padding: 0px;
}
QListWidget::item:selected {
background-color: transparent;
}
""")
layout.addWidget(self.partners_list)
# Кнопка расчета материалов
self.calc_button = QPushButton("Калькулятор материалов")
self.calc_button.clicked.connect(self.show_material_calculator)
self.calc_button.setStyleSheet("""
QPushButton {
background-color: #17a2b8;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #138496;
}
""")
layout.addWidget(self.calc_button)
panel.setLayout(layout)
return panel
def create_details_panel(self):
"""Создание панели детальной информации"""
panel = QWidget()
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Заголовок детальной информации
self.details_title = QLabel("Выберите партнера")
self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold))
self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.details_title.setStyleSheet("padding: 10px;")
layout.addWidget(self.details_title)
# Детальная информация о партнере - создаем пустой frame
self.details_frame = QFrame()
self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
self.details_frame.setStyleSheet("""
QFrame {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
}
""")
self.details_layout = QVBoxLayout()
self.details_layout.setSpacing(8)
self.details_frame.setLayout(self.details_layout)
self.details_frame.hide()
layout.addWidget(self.details_frame)
# Кнопки управления выбранным партнером
self.control_buttons = QWidget()
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(10)
self.edit_button = QPushButton("Редактировать")
self.edit_button.clicked.connect(self.edit_partner)
self.edit_button.setStyleSheet("""
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.edit_button.hide()
self.sales_button = QPushButton("История продаж")
self.sales_button.clicked.connect(self.show_sales_history)
self.sales_button.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #218838;
}
""")
self.sales_button.hide()
self.discount_button = QPushButton("Расчет скидки")
self.discount_button.clicked.connect(self.calculate_discount)
self.discount_button.setStyleSheet("""
QPushButton {
background-color: #ffc107;
color: black;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #e0a800;
}
""")
self.discount_button.hide()
buttons_layout.addWidget(self.edit_button)
buttons_layout.addWidget(self.sales_button)
buttons_layout.addWidget(self.discount_button)
buttons_layout.addStretch()
self.control_buttons.setLayout(buttons_layout)
layout.addWidget(self.control_buttons)
# Добавляем растягивающийся элемент в конец
layout.addStretch()
panel.setLayout(layout)
return panel
def load_partners(self):
"""Загрузка списка партнеров из API с авторизацией"""
try:
response = requests.get(
"http://localhost:8000/api/v1/partners",
auth=self.auth,
timeout=10
)
if response.status_code == 200:
self.partners_list.clear()
partners = response.json()
for partner in partners:
item = QListWidgetItem()
card = PartnerCard(partner)
card.partner_clicked.connect(self.show_partner_details)
# Устанавливаем фиксированный размер для элемента
item.setSizeHint(card.sizeHint())
self.partners_list.addItem(item)
self.partners_list.setItemWidget(item, card)
# Сбрасываем выделение
self.partners_list.clearSelection()
self.current_partner = None
self.details_title.setText("Выберите партнера")
self.details_frame.hide()
self.edit_button.hide()
self.sales_button.hide()
self.discount_button.hide()
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
self.logout()
else:
QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров")
except requests.exceptions.ConnectionError:
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}")
def show_partner_details(self, partner_data):
"""Отображение детальной информации о партнере"""
self.current_partner = partner_data
self.details_title.setText(partner_data['company_name'])
# Создаем новый виджет для деталей вместо очистки layout
new_details_frame = QFrame()
new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
new_details_frame.setStyleSheet("""
QFrame {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
}
""")
new_details_layout = QVBoxLayout()
new_details_layout.setSpacing(8)
# Добавляем новую информацию
details = [
("Тип:", partner_data.get('partner_type', 'Не указан')),
("ИНН:", partner_data.get('inn', 'Не указан')),
("Директор:", partner_data.get('director_name', 'Не указан')),
("Телефон:", partner_data.get('phone', 'Не указан')),
("Email:", partner_data.get('email', 'Не указан')),
("Рейтинг:", str(partner_data.get('rating', 0))),
("Адрес:", partner_data.get('legal_address', 'Не указан')),
("Регионы:", partner_data.get('sales_locations', 'Не указан'))
]
# ЗАМЕНИТЕ этот блок кода в методе show_partner_details:
for label, value in details:
row_widget = QWidget()
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(0, 2, 0, 2)
label_widget = QLabel(label)
label_widget.setStyleSheet("font-weight: bold; min-width: 100px;")
label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
value_widget = QLabel(str(value))
value_widget.setWordWrap(True)
value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
row_layout.addWidget(label_widget)
row_layout.addWidget(value_widget)
row_layout.addStretch()
new_details_layout.addWidget(row_widget)
# НА этот исправленный вариант:
for label, value in details:
# Создаем контейнер для строки
row_container = QWidget()
row_container.setFixedHeight(30) # Фиксированная высота для каждой строки
row_layout = QHBoxLayout(row_container)
row_layout.setContentsMargins(5, 0, 5, 0)
row_layout.setSpacing(10)
# Лейбл (название поля)
label_widget = QLabel(label)
label_widget.setStyleSheet("""
QLabel {
font-weight: bold;
color: #333;
min-width: 120px;
max-width: 120px;
background-color: transparent;
}
""")
label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
# Значение
value_widget = QLabel(str(value))
value_widget.setStyleSheet("""
QLabel {
color: #555;
background-color: transparent;
border: none;
}
""")
value_widget.setWordWrap(True)
value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
row_layout.addWidget(label_widget)
row_layout.addWidget(value_widget)
row_layout.addStretch()
new_details_layout.addWidget(row_container)
new_details_frame.setLayout(new_details_layout)
# Заменяем старый details_frame на новый
old_frame = self.details_frame
layout = self.right_panel.layout()
layout.replaceWidget(old_frame, new_details_frame)
old_frame.deleteLater()
self.details_frame = new_details_frame
self.details_layout = new_details_layout
self.details_frame.show()
self.edit_button.show()
self.sales_button.show()
self.discount_button.show()
def show_add_partner_form(self):
"""Открытие формы добавления партнера"""
form = PartnerForm(self, auth=self.auth)
form.partner_saved.connect(self.load_partners)
form.exec()
def edit_partner(self):
"""Редактирование выбранного партнера"""
if self.current_partner:
form = PartnerForm(self, self.current_partner, auth=self.auth)
form.partner_saved.connect(self.load_partners)
form.exec()
def show_sales_history(self):
"""Открытие истории продаж партнера"""
if self.current_partner:
sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth)
sales_window.exec()
def calculate_discount(self):
"""Расчет скидки для партнера с авторизацией"""
if self.current_partner:
try:
response = requests.get(
f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount",
auth=self.auth,
timeout=10
)
if response.status_code == 200:
discount_data = response.json()
QMessageBox.information(
self,
"Расчет скидки",
f"Партнер: {self.current_partner['company_name']}\n"
f"Общие продажи: {discount_data['total_sales']}\n"
f"Скидка: {discount_data['discount_percent']}%"
)
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
self.logout()
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}")
def show_material_calculator(self):
"""Открытие калькулятора материалов"""
calculator = MaterialCalculatorWindow(self, auth=self.auth)
calculator.exec()
def logout(self):
"""Выход из системы"""
reply = QMessageBox.question(
self,
"Подтверждение выхода",
"Вы уверены, что хотите выйти из системы?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.close()
# Здесь можно добавить вызов окна авторизации
# или перезапуск приложения
def show_about(self):
"""Показать информацию о программе"""
QMessageBox.about(
self,
"О программе MasterPol",
"MasterPol - Система управления партнерами\n\n"
"Версия: 1.0.0\n"
"Разработчик: Команда MasterPol\n\n"
"Система предназначена для управления партнерами,\n"
"учета продаж и расчета бизнес-показателей."
)

View file

@ -0,0 +1,160 @@
# gui/material_calculator.py
"""
Калькулятор материалов для производства
Соответствует модулю 4 ТЗ
"""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QMessageBox, QFormLayout,
QDoubleSpinBox, QSpinBox)
from PyQt6.QtCore import Qt
import requests
import math
class MaterialCalculatorWindow(QDialog):
def __init__(self, parent=None, auth=None):
super().__init__(parent)
self.auth = auth # Сохраняем auth, даже если не используется
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Калькулятор материалов для производства")
self.setModal(True)
self.resize(400, 300)
layout = QVBoxLayout()
# Заголовок
title = QLabel("Расчет количества материала")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
layout.addWidget(title)
# Форма ввода параметров
form_layout = QFormLayout()
self.product_type_id = QSpinBox()
self.product_type_id.setRange(1, 100)
form_layout.addRow("ID типа продукции:", self.product_type_id)
self.material_type_id = QSpinBox()
self.material_type_id.setRange(1, 100)
form_layout.addRow("ID типа материала:", self.material_type_id)
self.quantity = QSpinBox()
self.quantity.setRange(1, 1000000)
form_layout.addRow("Количество продукции:", self.quantity)
self.param1 = QDoubleSpinBox()
self.param1.setRange(0.1, 1000.0)
self.param1.setDecimals(2)
form_layout.addRow("Параметр продукции 1:", self.param1)
self.param2 = QDoubleSpinBox()
self.param2.setRange(0.1, 1000.0)
self.param2.setDecimals(2)
form_layout.addRow("Параметр продукции 2:", self.param2)
self.product_coeff = QDoubleSpinBox()
self.product_coeff.setRange(0.1, 10.0)
self.product_coeff.setDecimals(3)
self.product_coeff.setValue(1.0)
form_layout.addRow("Коэффициент типа продукции:", self.product_coeff)
self.defect_percent = QDoubleSpinBox()
self.defect_percent.setRange(0.0, 50.0)
self.defect_percent.setDecimals(1)
self.defect_percent.setSuffix("%")
form_layout.addRow("Процент брака материала:", self.defect_percent)
layout.addLayout(form_layout)
# Результат
self.result_label = QLabel()
self.result_label.setStyleSheet("font-weight: bold; color: #007acc; margin: 10px;")
self.result_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.result_label)
# Кнопки
buttons_layout = QHBoxLayout()
self.calculate_button = QPushButton("Рассчитать")
self.calculate_button.clicked.connect(self.calculate_material)
self.calculate_button.setStyleSheet("""
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.close_button = QPushButton("Закрыть")
self.close_button.clicked.connect(self.accept)
buttons_layout.addWidget(self.calculate_button)
buttons_layout.addStretch()
buttons_layout.addWidget(self.close_button)
layout.addLayout(buttons_layout)
self.setLayout(layout)
def calculate_material(self):
"""Расчет количества материала с обработкой ошибок"""
try:
# Проверяем валидность входных данных
if (self.param1.value() <= 0 or self.param2.value() <= 0 or
self.product_coeff.value() <= 0 or self.defect_percent.value() < 0):
self.result_label.setText("Ошибка: неверные параметры")
self.result_label.setStyleSheet("color: red; font-weight: bold;")
return
# Создаем данные для расчета
calculation_data = {
'product_type_id': self.product_type_id.value(),
'material_type_id': self.material_type_id.value(),
'quantity': self.quantity.value(),
'param1': self.param1.value(),
'param2': self.param2.value(),
'product_coeff': self.product_coeff.value(),
'defect_percent': self.defect_percent.value()
}
# Локальный расчет (без API)
material_quantity = self.calculate_locally(calculation_data)
if material_quantity >= 0:
self.result_label.setText(
f"Необходимое количество материала: {material_quantity} единиц"
)
self.result_label.setStyleSheet("color: #007acc; font-weight: bold;")
else:
self.result_label.setText("Ошибка: неверные параметры расчета")
self.result_label.setStyleSheet("color: red; font-weight: bold;")
except Exception as e:
self.result_label.setText(f"Ошибка расчета: {str(e)}")
self.result_label.setStyleSheet("color: red; font-weight: bold;")
def calculate_locally(self, data):
"""Локальный расчет материалов"""
try:
import math
# Расчет количества материала на одну единицу продукции
material_per_unit = data['param1'] * data['param2'] * data['product_coeff']
# Расчет общего количества материала с учетом брака
total_material = material_per_unit * data['quantity']
total_material_with_defect = total_material * (1 + data['defect_percent'] / 100)
# Округление до целого числа в большую сторону
return math.ceil(total_material_with_defect)
except:
return -1

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

@ -0,0 +1,344 @@
# gui/orders_panel.py
"""
Панель управления заказами и продажами
"""
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QTableWidget, QTableWidgetItem, QPushButton,
QHeaderView, QMessageBox, QDateEdit, QComboBox,
QLineEdit, QFormLayout, QDialog, QDoubleSpinBox)
from PyQt6.QtCore import Qt, QDate
from PyQt6.QtGui import QFont
import requests
class OrderForm(QDialog):
"""Форма для добавления/редактирования заказа"""
order_saved = pyqtSignal()
def __init__(self, parent=None, order_data=None, auth=None, partners=None):
super().__init__(parent)
self.order_data = order_data
self.auth = auth
self.partners = partners or []
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Добавить заказ" if not self.order_data else "Редактировать заказ")
self.setModal(True)
self.resize(400, 300)
layout = QVBoxLayout()
# Форма ввода данных
form_layout = QFormLayout()
# Выбор партнера
self.partner_combo = QComboBox()
self.partner_combo.addItem("Выберите партнера", None)
for partner in self.partners:
self.partner_combo.addItem(partner['company_name'], partner['partner_id'])
form_layout.addRow("Партнер*:", self.partner_combo)
# Название продукта
self.product_name = QLineEdit()
self.product_name.setPlaceholderText("Введите название продукта")
form_layout.addRow("Продукт*:", self.product_name)
# Количество
self.quantity = QDoubleSpinBox()
self.quantity.setRange(0.01, 100000.0)
self.quantity.setDecimals(2)
form_layout.addRow("Количество*:", self.quantity)
# Дата продажи
self.sale_date = QDateEdit()
self.sale_date.setDate(QDate.currentDate())
self.sale_date.setCalendarPopup(True)
form_layout.addRow("Дата продажи*:", self.sale_date)
layout.addLayout(form_layout)
# Кнопки
buttons_layout = QHBoxLayout()
self.save_button = QPushButton("Сохранить")
self.save_button.clicked.connect(self.save_order)
self.save_button.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #218838;
}
""")
self.cancel_button = QPushButton("Отмена")
self.cancel_button.clicked.connect(self.reject)
buttons_layout.addWidget(self.save_button)
buttons_layout.addWidget(self.cancel_button)
buttons_layout.addStretch()
layout.addLayout(buttons_layout)
self.setLayout(layout)
# Если редактирование, заполняем форму
if self.order_data:
self.fill_form()
def fill_form(self):
"""Заполнение формы данными заказа"""
data = self.order_data
# Устанавливаем партнера
partner_index = self.partner_combo.findData(data.get('partner_id'))
if partner_index >= 0:
self.partner_combo.setCurrentIndex(partner_index)
self.product_name.setText(data.get('product_name', ''))
self.quantity.setValue(float(data.get('quantity', 0)))
# Устанавливаем дату
sale_date = data.get('sale_date')
if sale_date:
date = QDate.fromString(sale_date, 'yyyy-MM-dd')
if date.isValid():
self.sale_date.setDate(date)
def validate_form(self):
"""Валидация данных формы"""
errors = []
if not self.partner_combo.currentData():
errors.append("Выберите партнера")
if not self.product_name.text().strip():
errors.append("Введите название продукта")
if self.quantity.value() <= 0:
errors.append("Количество должно быть больше 0")
return errors
def save_order(self):
"""Сохранение заказа"""
errors = self.validate_form()
if errors:
QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors))
return
order_data = {
'partner_id': self.partner_combo.currentData(),
'product_name': self.product_name.text().strip(),
'quantity': self.quantity.value(),
'sale_date': self.sale_date.date().toString('yyyy-MM-dd')
}
try:
if self.order_data:
# Обновление существующего заказа
response = requests.put(
f"http://localhost:8000/api/v1/sales/{self.order_data['sale_id']}",
json=order_data,
auth=self.auth,
timeout=10
)
else:
# Создание нового заказа
response = requests.post(
"http://localhost:8000/api/v1/sales",
json=order_data,
auth=self.auth,
timeout=10
)
if response.status_code == 200:
self.order_saved.emit()
QMessageBox.information(self, "Успех", "Заказ успешно сохранен")
self.accept()
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
else:
error_msg = response.json().get('detail', 'Неизвестная ошибка')
QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить заказ: {error_msg}")
except requests.exceptions.ConnectionError:
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}")
class OrdersPanel(QWidget):
"""Панель управления заказами"""
def __init__(self, auth=None):
super().__init__()
self.auth = auth
self.partners = []
self.setup_ui()
self.load_partners()
self.load_orders()
def setup_ui(self):
"""Настройка интерфейса панели заказов"""
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Заголовок
title = QLabel("Управление заказами")
title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title)
# Панель управления
control_layout = QHBoxLayout()
self.add_button = QPushButton("Добавить заказ")
self.add_button.clicked.connect(self.show_add_order_form)
self.add_button.setStyleSheet("""
QPushButton {
background-color: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.refresh_button = QPushButton("Обновить")
self.refresh_button.clicked.connect(self.load_orders)
control_layout.addWidget(self.add_button)
control_layout.addWidget(self.refresh_button)
control_layout.addStretch()
layout.addLayout(control_layout)
# Таблица заказов
self.orders_table = QTableWidget()
self.orders_table.setColumnCount(6)
self.orders_table.setHorizontalHeaderLabels([
"ID", "Партнер", "Продукт", "Количество", "Дата", "Действия"
])
self.orders_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.orders_table.setStyleSheet("""
QTableWidget {
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
}
QTableWidget::item {
padding: 8px;
}
""")
layout.addWidget(self.orders_table)
self.setLayout(layout)
def load_partners(self):
"""Загрузка списка партнеров"""
try:
response = requests.get(
"http://localhost:8000/api/v1/partners",
auth=self.auth,
timeout=10
)
if response.status_code == 200:
self.partners = response.json()
except:
self.partners = []
def load_orders(self):
"""Загрузка списка заказов"""
try:
response = requests.get(
"http://localhost:8000/api/v1/sales",
auth=self.auth,
timeout=10
)
if response.status_code == 200:
orders = response.json()
self.display_orders(orders)
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла")
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить заказы: {str(e)}")
def display_orders(self, orders):
"""Отображение заказов в таблице"""
self.orders_table.setRowCount(len(orders))
for row, order in enumerate(orders):
self.orders_table.setItem(row, 0, QTableWidgetItem(str(order.get('sale_id', ''))))
self.orders_table.setItem(row, 1, QTableWidgetItem(order.get('company_name', 'Неизвестно')))
self.orders_table.setItem(row, 2, QTableWidgetItem(order.get('product_name', '')))
self.orders_table.setItem(row, 3, QTableWidgetItem(str(order.get('quantity', ''))))
self.orders_table.setItem(row, 4, QTableWidgetItem(order.get('sale_date', '')))
# Кнопки действий
actions_widget = QWidget()
actions_layout = QHBoxLayout(actions_widget)
actions_layout.setContentsMargins(4, 4, 4, 4)
actions_layout.setSpacing(4)
delete_button = QPushButton("Удалить")
delete_button.setStyleSheet("""
QPushButton {
background-color: #dc3545;
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
font-size: 11px;
}
QPushButton:hover {
background-color: #c82333;
}
""")
delete_button.clicked.connect(lambda checked, o=order: self.delete_order(o))
actions_layout.addWidget(delete_button)
actions_layout.addStretch()
self.orders_table.setCellWidget(row, 5, actions_widget)
def show_add_order_form(self):
"""Открытие формы добавления заказа"""
form = OrderForm(self, auth=self.auth, partners=self.partners)
form.order_saved.connect(self.load_orders)
form.exec()
def delete_order(self, order):
"""Удаление заказа"""
reply = QMessageBox.question(
self,
"Подтверждение удаления",
f"Вы уверены, что хотите удалить заказ #{order.get('sale_id')}?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
try:
response = requests.delete(
f"http://localhost:8000/api/v1/sales/{order['sale_id']}",
auth=self.auth,
timeout=10
)
if response.status_code == 200:
self.load_orders()
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла")
except Exception as e:
QMessageBox.warning(self, "Ошибка", f"Не удалось удалить заказ: {str(e)}")

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

@ -0,0 +1,193 @@
# gui/partner_form.py (обновленный)
"""
Форма для добавления/редактирования партнера с поддержкой авторизации
"""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QComboBox, QPushButton, QMessageBox,
QFormLayout, QSpinBox)
from PyQt6.QtCore import pyqtSignal
import requests
class PartnerForm(QDialog):
partner_saved = pyqtSignal()
def __init__(self, parent=None, partner_data=None, auth=None):
super().__init__(parent)
self.partner_data = partner_data
self.auth = auth
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Добавить партнера" if not self.partner_data else "Редактировать партнера")
self.setModal(True)
self.resize(500, 400)
layout = QVBoxLayout()
# Форма ввода данных
form_layout = QFormLayout()
self.company_name = QLineEdit()
self.company_name.setPlaceholderText("Введите наименование компании")
form_layout.addRow("Наименование компании*:", self.company_name)
self.inn = QLineEdit()
self.inn.setPlaceholderText("Введите ИНН")
form_layout.addRow("ИНН*:", self.inn)
self.partner_type = QComboBox()
self.partner_type.addItems(["", "distributor", "retail", "wholesale", "dealer"])
self.partner_type.setPlaceholderText("Выберите тип партнера")
form_layout.addRow("Тип партнера:", self.partner_type)
self.rating = QSpinBox()
self.rating.setRange(0, 100)
self.rating.setSuffix("%")
form_layout.addRow("Рейтинг:", self.rating)
self.legal_address = QLineEdit()
self.legal_address.setPlaceholderText("Введите юридический адрес")
form_layout.addRow("Юридический адрес:", self.legal_address)
self.director_name = QLineEdit()
self.director_name.setPlaceholderText("Введите ФИО директора")
form_layout.addRow("ФИО директора:", self.director_name)
self.phone = QLineEdit()
self.phone.setPlaceholderText("+7XXXXXXXXXX")
form_layout.addRow("Телефон:", self.phone)
self.email = QLineEdit()
self.email.setPlaceholderText("email@example.com")
form_layout.addRow("Email:", self.email)
self.sales_locations = QLineEdit()
self.sales_locations.setPlaceholderText("Москва, Санкт-Петербург...")
form_layout.addRow("Регионы продаж:", self.sales_locations)
layout.addLayout(form_layout)
# Кнопки
buttons_layout = QHBoxLayout()
self.save_button = QPushButton("Сохранить")
self.save_button.clicked.connect(self.save_partner)
self.save_button.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #218838;
}
""")
self.cancel_button = QPushButton("Отмена")
self.cancel_button.clicked.connect(self.reject)
buttons_layout.addWidget(self.save_button)
buttons_layout.addWidget(self.cancel_button)
buttons_layout.addStretch()
layout.addLayout(buttons_layout)
self.setLayout(layout)
# Если редактирование, заполняем форму
if self.partner_data:
self.fill_form()
def fill_form(self):
"""Заполнение формы данными партнера"""
data = self.partner_data
self.company_name.setText(data.get('company_name', ''))
self.inn.setText(data.get('inn', ''))
partner_type = data.get('partner_type', '')
if partner_type:
index = self.partner_type.findText(partner_type)
if index >= 0:
self.partner_type.setCurrentIndex(index)
# Безопасное преобразование рейтинга
rating = data.get('rating', 0)
if isinstance(rating, float):
rating = int(rating)
self.rating.setValue(rating)
self.legal_address.setText(data.get('legal_address', ''))
self.director_name.setText(data.get('director_name', ''))
self.phone.setText(data.get('phone', ''))
self.email.setText(data.get('email', ''))
self.sales_locations.setText(data.get('sales_locations', ''))
def validate_form(self):
"""Валидация данных формы"""
errors = []
if not self.company_name.text().strip():
errors.append("Наименование компании обязательно")
if not self.inn.text().strip():
errors.append("ИНН обязателен")
if self.phone.text() and not self.phone.text().startswith('+'):
errors.append("Телефон должен начинаться с '+'")
return errors
def save_partner(self):
"""Сохранение партнера с авторизацией"""
errors = self.validate_form()
if errors:
QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors))
return
partner_data = {
'company_name': self.company_name.text().strip(),
'inn': self.inn.text().strip(),
'partner_type': self.partner_type.currentText() or None,
'rating': self.rating.value(),
'legal_address': self.legal_address.text().strip() or None,
'director_name': self.director_name.text().strip() or None,
'phone': self.phone.text().strip() or None,
'email': self.email.text().strip() or None,
'sales_locations': self.sales_locations.text().strip() or None
}
try:
if self.partner_data:
# Обновление существующего партнера
response = requests.put(
f"http://localhost:8000/api/v1/partners/{self.partner_data['partner_id']}",
json=partner_data,
auth=self.auth,
timeout=10
)
else:
# Создание нового партнера
response = requests.post(
"http://localhost:8000/api/v1/partners",
json=partner_data,
auth=self.auth,
timeout=10
)
if response.status_code == 200:
self.partner_saved.emit()
QMessageBox.information(self, "Успех", "Партнер успешно сохранен")
self.accept()
elif response.status_code == 401:
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
else:
error_msg = response.json().get('detail', 'Неизвестная ошибка')
QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить партнера: {error_msg}")
except requests.exceptions.ConnectionError:
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}")

View file

@ -0,0 +1,186 @@
# gui/partner_form.py
"""
Форма для добавления/редактирования партнера
Соответствует модулю 3 ТЗ
"""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QComboBox, QPushButton, QMessageBox,
QFormLayout, QSpinBox)
from PyQt6.QtCore import pyqtSignal
import requests
class PartnerForm(QDialog):
partner_saved = pyqtSignal()
def __init__(self, parent=None, partner_data=None):
super().__init__(parent)
self.partner_data = partner_data
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Добавить партнера" if not self.partner_data else "Редактировать партнера")
self.setModal(True)
self.resize(500, 400)
layout = QVBoxLayout()
# Форма ввода данных
form_layout = QFormLayout()
self.company_name = QLineEdit()
self.company_name.setPlaceholderText("Введите наименование компании")
form_layout.addRow("Наименование компании*:", self.company_name)
self.inn = QLineEdit()
self.inn.setPlaceholderText("Введите ИНН")
form_layout.addRow("ИНН*:", self.inn)
self.partner_type = QComboBox()
self.partner_type.addItems(["", "distributor", "retail", "wholesale", "dealer"])
self.partner_type.setPlaceholderText("Выберите тип партнера")
form_layout.addRow("Тип партнера:", self.partner_type)
self.rating = QSpinBox()
self.rating.setRange(0, 100)
self.rating.setSuffix("%")
form_layout.addRow("Рейтинг:", self.rating)
self.legal_address = QLineEdit()
self.legal_address.setPlaceholderText("Введите юридический адрес")
form_layout.addRow("Юридический адрес:", self.legal_address)
self.director_name = QLineEdit()
self.director_name.setPlaceholderText("Введите ФИО директора")
form_layout.addRow("ФИО директора:", self.director_name)
self.phone = QLineEdit()
self.phone.setPlaceholderText("+7XXXXXXXXXX")
form_layout.addRow("Телефон:", self.phone)
self.email = QLineEdit()
self.email.setPlaceholderText("email@example.com")
form_layout.addRow("Email:", self.email)
self.sales_locations = QLineEdit()
self.sales_locations.setPlaceholderText("Москва, Санкт-Петербург...")
form_layout.addRow("Регионы продаж:", self.sales_locations)
layout.addLayout(form_layout)
# Кнопки
buttons_layout = QHBoxLayout()
self.save_button = QPushButton("Сохранить")
self.save_button.clicked.connect(self.save_partner)
self.save_button.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #218838;
}
""")
self.cancel_button = QPushButton("Отмена")
self.cancel_button.clicked.connect(self.reject)
buttons_layout.addWidget(self.save_button)
buttons_layout.addWidget(self.cancel_button)
buttons_layout.addStretch()
layout.addLayout(buttons_layout)
self.setLayout(layout)
# Если редактирование, заполняем форму
if self.partner_data:
self.fill_form()
# gui/partner_form.py (исправленный метод fill_form)
def fill_form(self):
"""Заполнение формы данными партнера"""
data = self.partner_data
self.company_name.setText(data.get('company_name', ''))
self.inn.setText(data.get('inn', ''))
partner_type = data.get('partner_type', '')
if partner_type:
index = self.partner_type.findText(partner_type)
if index >= 0:
self.partner_type.setCurrentIndex(index)
# Безопасное преобразование рейтинга к int
rating = data.get('rating', 0)
if isinstance(rating, float):
rating = int(rating)
self.rating.setValue(rating)
self.legal_address.setText(data.get('legal_address', ''))
self.director_name.setText(data.get('director_name', ''))
self.phone.setText(data.get('phone', ''))
self.email.setText(data.get('email', ''))
self.sales_locations.setText(data.get('sales_locations', ''))
def validate_form(self):
"""Валидация данных формы"""
errors = []
if not self.company_name.text().strip():
errors.append("Наименование компании обязательно")
if not self.inn.text().strip():
errors.append("ИНН обязателен")
if self.phone.text() and not self.phone.text().startswith('+'):
errors.append("Телефон должен начинаться с '+'")
return errors
def save_partner(self):
"""Сохранение партнера"""
errors = self.validate_form()
if errors:
QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors))
return
partner_data = {
'company_name': self.company_name.text().strip(),
'inn': self.inn.text().strip(),
'partner_type': self.partner_type.currentText() or None,
'rating': self.rating.value(),
'legal_address': self.legal_address.text().strip() or None,
'director_name': self.director_name.text().strip() or None,
'phone': self.phone.text().strip() or None,
'email': self.email.text().strip() or None,
'sales_locations': self.sales_locations.text().strip() or None
}
try:
if self.partner_data:
# Обновление существующего партнера
response = requests.put(
f"http://localhost:8000/api/v1/partners/{self.partner_data['partner_id']}",
json=partner_data
)
else:
# Создание нового партнера
response = requests.post(
"http://localhost:8000/api/v1/partners",
json=partner_data
)
if response.status_code == 200:
self.partner_saved.emit()
QMessageBox.information(self, "Успех", "Партнер успешно сохранен")
self.accept()
else:
error_msg = response.json().get('detail', 'Неизвестная ошибка')
QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить партнера: {error_msg}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}")

View file

@ -0,0 +1,91 @@
# gui/sales_history.py
"""
Окно истории продаж партнера
Соответствует модулю 4 ТЗ
"""
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QTableWidget, QTableWidgetItem, QPushButton,
QHeaderView, QMessageBox)
from PyQt6.QtCore import Qt
import requests
class SalesHistoryWindow(QDialog):
def __init__(self, partner_data, parent=None):
super().__init__(parent)
self.partner_data = partner_data
self.setup_ui()
self.load_sales_history()
def setup_ui(self):
self.setWindowTitle(f"История продаж - {self.partner_data['company_name']}")
self.setModal(True)
self.resize(800, 400)
layout = QVBoxLayout()
# Заголовок
title = QLabel(f"История реализации продукции\n{self.partner_data['company_name']}")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
layout.addWidget(title)
# Таблица продаж
self.sales_table = QTableWidget()
self.sales_table.setColumnCount(4)
self.sales_table.setHorizontalHeaderLabels([
"ID", "Наименование продукции", "Количество", "Дата продажи"
])
self.sales_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
layout.addWidget(self.sales_table)
# Статистика
self.stats_label = QLabel()
self.stats_label.setStyleSheet("font-weight: bold; margin: 10px;")
layout.addWidget(self.stats_label)
# Кнопки
buttons_layout = QHBoxLayout()
self.close_button = QPushButton("Закрыть")
self.close_button.clicked.connect(self.accept)
buttons_layout.addStretch()
buttons_layout.addWidget(self.close_button)
layout.addLayout(buttons_layout)
self.setLayout(layout)
def load_sales_history(self):
"""Загрузка истории продаж партнера"""
try:
response = requests.get(
f"http://localhost:8000/api/v1/sales/partner/{self.partner_data['partner_id']}"
)
if response.status_code == 200:
sales_data = response.json()
self.display_sales_data(sales_data)
else:
QMessageBox.warning(self, "Ошибка", "Не удалось загрузить историю продаж")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}")
def display_sales_data(self, sales_data):
"""Отображение данных о продажах в таблице"""
self.sales_table.setRowCount(len(sales_data))
total_quantity = 0
for row, sale in enumerate(sales_data):
self.sales_table.setItem(row, 0, QTableWidgetItem(str(sale['sale_id'])))
self.sales_table.setItem(row, 1, QTableWidgetItem(sale['product_name']))
self.sales_table.setItem(row, 2, QTableWidgetItem(str(sale['quantity'])))
self.sales_table.setItem(row, 3, QTableWidgetItem(sale['sale_date']))
total_quantity += float(sale['quantity'])
# Обновление статистики
self.stats_label.setText(
f"Общее количество проданной продукции: {total_quantity}\n"
f"Всего продаж: {len(sales_data)}"
)