diff --git a/fitness.db b/fitness.db index 4222c95..6659418 100644 Binary files a/fitness.db and b/fitness.db differ diff --git a/main3.py b/main3.py new file mode 100644 index 0000000..553ac1f --- /dev/null +++ b/main3.py @@ -0,0 +1,2461 @@ +import sys +import sqlite3 +import os +import math +from datetime import datetime, date +from decimal import Decimal + +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QTextEdit, QPushButton, QTableWidget, QTableWidgetItem, + QHeaderView, QMessageBox, QDialog, QFormLayout, QGroupBox, QTabWidget, + QComboBox, QDateEdit, QSpinBox, QDoubleSpinBox, QCheckBox, QStackedWidget +) +from PyQt6.QtCore import Qt, QRegularExpression, QDate +from PyQt6.QtGui import QRegularExpressionValidator, QFont, QPixmap, QIcon +from PyQt6.QtSql import QSqlDatabase, QSqlQuery + + +# === Стили приложения === +APP_STYLES = { + 'primary_bg': '#FFFFFF', + 'secondary_bg': '#F4E8D3', + 'accent_color': '#67BA80', + 'font_family': 'Segoe UI' +} + + +# === Функция расчета скидки === +def calculate_partner_discount(rating, total_sales, sales_count): + """ + Расчет скидки для партнера на основе рейтинга и количества продаж + + Формула: + - Базовая скидка от рейтинга: rating * 2 (макс 10%) + - Бонус за количество продаж: log10(sales_count + 1) * 3 (макс 15%) + - Максимальная общая скидка: 25% + + Args: + rating (float): Рейтинг партнера от 0.00 до 5.00 + total_sales (float): Общая сумма продаж + sales_count (int): Количество совершенных продаж + + Returns: + float: Размер скидки в процентах + """ + # Базовая скидка от рейтинга (0-10%) + rating_discount = min(rating * 2.0, 10.0) + + # Бонусная скидка от количества продаж (0-15%) + # Логарифмическая зависимость - чем больше продаж, тем медленнее рост скидки + if sales_count > 0: + sales_bonus = min(math.log10(sales_count + 1) * 3.0, 15.0) + else: + sales_bonus = 0.0 + + # Дополнительный бонус за крупные суммы продаж + volume_bonus = 0.0 + if total_sales > 5000000: # 5 млн руб + volume_bonus = 2.0 + elif total_sales > 2000000: # 2 млн руб + volume_bonus = 1.0 + elif total_sales > 1000000: # 1 млн руб + volume_bonus = 0.5 + + total_discount = rating_discount + sales_bonus + volume_bonus + + # Ограничение максимальной скидки 25% + return min(total_discount, 25.0) + + +# === Функция обновления скидок всех партнеров === +def update_all_partners_discounts(): + """Обновление скидок для всех партнеров на основе актуальных данных""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + # Получаем актуальные данные по продажам для каждого партнера + cursor.execute(""" + SELECT + p.partner_id, + p.rating, + COALESCE(SUM(o.final_amount), 0) as total_sales, + COUNT(o.order_id) as sales_count + FROM partners p + LEFT JOIN orders o ON p.partner_id = o.partner_id AND o.status = 'COMPLETED' + GROUP BY p.partner_id + """) + + partners_data = cursor.fetchall() + + updated_count = 0 + for partner_id, rating, total_sales, sales_count in partners_data: + # Рассчитываем новую скидку + new_discount = calculate_partner_discount( + float(rating) if rating else 0.0, + float(total_sales) if total_sales else 0.0, + sales_count + ) + + # Обновляем скидку в базе + cursor.execute(""" + UPDATE partners + SET discount_rate = ?, total_sales = ? + WHERE partner_id = ? + """, (new_discount, total_sales, partner_id)) + + updated_count += 1 + + conn.commit() + return updated_count + + except sqlite3.Error as e: + conn.rollback() + raise e + finally: + conn.close() + + +# === Функция обновления скидки конкретного партнера === +def update_partner_discount(partner_id): + """Обновление скидки для конкретного партнера""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + # Получаем актуальные данные по продажам партнера + cursor.execute(""" + SELECT + p.rating, + COALESCE(SUM(o.final_amount), 0) as total_sales, + COUNT(o.order_id) as sales_count + FROM partners p + LEFT JOIN orders o ON p.partner_id = o.partner_id AND o.status = 'COMPLETED' + WHERE p.partner_id = ? + GROUP BY p.partner_id + """, (partner_id,)) + + data = cursor.fetchone() + + if data: + rating, total_sales, sales_count = data + + # Рассчитываем новую скидку + new_discount = calculate_partner_discount( + float(rating) if rating else 0.0, + float(total_sales) if total_sales else 0.0, + sales_count + ) + + # Обновляем скидку в базе + cursor.execute(""" + UPDATE partners + SET discount_rate = ?, total_sales = ? + WHERE partner_id = ? + """, (new_discount, total_sales, partner_id)) + + conn.commit() + return new_discount + + return None + + except sqlite3.Error as e: + conn.rollback() + raise e + finally: + conn.close() + + +# === Инициализация базы данных SQLite === +def init_database(): + """Инициализация базы данных SQLite со всеми таблицами""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + # Включаем иностранные ключи + cursor.execute("PRAGMA foreign_keys = ON") + + # Таблица партнеров + cursor.execute(""" + CREATE TABLE IF NOT EXISTS partners ( + partner_id INTEGER PRIMARY KEY AUTOINCREMENT, + partner_type VARCHAR(50) NOT NULL, + company_name VARCHAR(255) NOT NULL, + legal_address TEXT, + inn VARCHAR(12) NOT NULL UNIQUE, + director_name VARCHAR(255), + phone VARCHAR(20), + email VARCHAR(255), + rating DECIMAL(3,2) CHECK (rating BETWEEN 0.00 AND 5.00), + sales_locations TEXT, + total_sales DECIMAL(15,2) DEFAULT 0, + discount_rate DECIMAL(5,2) DEFAULT 0, + created_date DATE DEFAULT CURRENT_DATE + ) + """) + + # Таблица сотрудников + cursor.execute(""" + CREATE TABLE IF NOT EXISTS employees ( + employee_id INTEGER PRIMARY KEY AUTOINCREMENT, + full_name VARCHAR(255) NOT NULL, + birth_date DATE, + passport_data TEXT, + bank_details TEXT, + has_family BOOLEAN DEFAULT FALSE, + health_info TEXT, + position VARCHAR(100), + hire_date DATE DEFAULT CURRENT_DATE, + salary DECIMAL(10,2), + is_active BOOLEAN DEFAULT TRUE + ) + """) + + # Таблица оборудования и доступов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS equipment_access ( + access_id INTEGER PRIMARY KEY AUTOINCREMENT, + employee_id INTEGER, + equipment_name VARCHAR(255) NOT NULL, + access_level VARCHAR(50), + granted_date DATE DEFAULT CURRENT_DATE, + FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE CASCADE + ) + """) + + # Таблица поставщиков + cursor.execute(""" + CREATE TABLE IF NOT EXISTS suppliers ( + supplier_id INTEGER PRIMARY KEY AUTOINCREMENT, + supplier_type VARCHAR(50), + company_name VARCHAR(255) NOT NULL, + inn VARCHAR(12) NOT NULL UNIQUE, + contact_person VARCHAR(255), + phone VARCHAR(20), + email VARCHAR(255), + rating DECIMAL(3,2) CHECK (rating BETWEEN 0.00 AND 5.00), + supplied_materials TEXT + ) + """) + + # Таблица материалов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS materials ( + material_id INTEGER PRIMARY KEY AUTOINCREMENT, + material_type VARCHAR(100) NOT NULL, + material_name VARCHAR(255) NOT NULL, + supplier_id INTEGER, + package_quantity DECIMAL(10,3), + unit_of_measure VARCHAR(50), + description TEXT, + cost_per_unit DECIMAL(10,2), + current_stock DECIMAL(10,3) DEFAULT 0, + min_stock_level DECIMAL(10,3) DEFAULT 0, + image_path TEXT, + FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id) + ) + """) + + # Таблица истории изменений запасов материалов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS material_stock_history ( + history_id INTEGER PRIMARY KEY AUTOINCREMENT, + material_id INTEGER NOT NULL, + change_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + change_type VARCHAR(20) NOT NULL, -- 'IN', 'OUT', 'ADJUST' + quantity DECIMAL(10,3) NOT NULL, + reason TEXT, + employee_id INTEGER, + FOREIGN KEY (material_id) REFERENCES materials(material_id), + FOREIGN KEY (employee_id) REFERENCES employees(employee_id) + ) + """) + + # Таблица продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS products ( + product_id INTEGER PRIMARY KEY AUTOINCREMENT, + article_number VARCHAR(100) UNIQUE NOT NULL, + product_type VARCHAR(100) NOT NULL, + product_name VARCHAR(255) NOT NULL, + description TEXT, + min_partner_price DECIMAL(10,2) NOT NULL, + package_length DECIMAL(8,2), + package_width DECIMAL(8,2), + package_height DECIMAL(8,2), + net_weight DECIMAL(8,2), + gross_weight DECIMAL(8,2), + certificate_path TEXT, + standard_number VARCHAR(100), + production_time_days INTEGER DEFAULT 1, + cost_price DECIMAL(10,2), + workshop_number INTEGER, + required_workers INTEGER, + is_active BOOLEAN DEFAULT TRUE + ) + """) + + # Таблица истории цен продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS product_price_history ( + price_history_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL, + change_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + old_price DECIMAL(10,2), + new_price DECIMAL(10,2) NOT NULL, + changed_by INTEGER, + reason TEXT, + FOREIGN KEY (product_id) REFERENCES products(product_id), + FOREIGN KEY (changed_by) REFERENCES employees(employee_id) + ) + """) + + # Таблица материалов для продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS product_materials ( + product_material_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL, + material_id INTEGER NOT NULL, + material_quantity DECIMAL(10,3) NOT NULL, + FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE, + FOREIGN KEY (material_id) REFERENCES materials(material_id) + ) + """) + + # Таблица заказов (заявок) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS orders ( + order_id INTEGER PRIMARY KEY AUTOINCREMENT, + partner_id INTEGER NOT NULL, + manager_id INTEGER NOT NULL, + order_date DATE DEFAULT CURRENT_DATE, + status VARCHAR(50) DEFAULT 'NEW', -- NEW, WAITING_PREPAYMENT, IN_PRODUCTION, READY_FOR_SHIPMENT, SHIPPED, COMPLETED, CANCELLED + total_amount DECIMAL(15,2), + discount_amount DECIMAL(15,2) DEFAULT 0, + final_amount DECIMAL(15,2), + prepayment_amount DECIMAL(15,2) DEFAULT 0, + prepayment_date DATE, + full_payment_date DATE, + expected_production_date DATE, + actual_production_date DATE, + delivery_method VARCHAR(100), + delivery_address TEXT, + notes TEXT, + cancellation_reason TEXT, + FOREIGN KEY (partner_id) REFERENCES partners(partner_id), + FOREIGN KEY (manager_id) REFERENCES employees(employee_id) + ) + """) + + # Таблица позиций заказа + cursor.execute(""" + CREATE TABLE IF NOT EXISTS order_items ( + order_item_id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + quantity DECIMAL(10,3) NOT NULL, + unit_price DECIMAL(10,2) NOT NULL, + total_price DECIMAL(15,2) NOT NULL, + production_cost DECIMAL(10,2), + FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products(product_id) + ) + """) + + # Таблица продаж (история реализации) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS sales ( + sale_id INTEGER PRIMARY KEY AUTOINCREMENT, + partner_id INTEGER NOT NULL, + product_name VARCHAR(255) NOT NULL, + quantity DECIMAL(10,3) NOT NULL CHECK (quantity > 0), + sale_date DATE NOT NULL DEFAULT CURRENT_DATE, + unit_price DECIMAL(10,2), + total_amount DECIMAL(15,2), + FOREIGN KEY (partner_id) REFERENCES partners(partner_id) ON DELETE CASCADE + ) + """) + + # Таблица истории продаж партнеров (для расчета скидок) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS partner_sales_history ( + history_id INTEGER PRIMARY KEY AUTOINCREMENT, + partner_id INTEGER NOT NULL, + period_start DATE NOT NULL, + period_end DATE NOT NULL, + total_sales DECIMAL(15,2) NOT NULL, + discount_rate DECIMAL(5,2) NOT NULL, + FOREIGN KEY (partner_id) REFERENCES partners(partner_id) ON DELETE CASCADE + ) + """) + + # Таблица запасов готовой продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS finished_goods_stock ( + stock_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL, + current_stock DECIMAL(10,3) DEFAULT 0, + reserved_stock DECIMAL(10,3) DEFAULT 0, + min_stock_level DECIMAL(10,3) DEFAULT 0, + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES products(product_id) + ) + """) + + # Таблица движения готовой продукции + cursor.execute(""" + CREATE TABLE IF NOT EXISTS finished_goods_movements ( + movement_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL, + movement_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + movement_type VARCHAR(20) NOT NULL, -- 'IN', 'OUT', 'RESERVE', 'UNRESERVE' + quantity DECIMAL(10,3) NOT NULL, + reference_id INTEGER, -- order_id or other reference + notes TEXT, + employee_id INTEGER, + FOREIGN KEY (product_id) REFERENCES products(product_id), + FOREIGN KEY (employee_id) REFERENCES employees(employee_id) + ) + """) + + # Добавляем тестовые данные + add_test_data(cursor) + + conn.commit() + conn.close() + + +def add_test_data(cursor): + """Добавление тестовых данных в базу""" + + # Добавляем менеджера + cursor.execute(""" + INSERT OR IGNORE INTO employees (employee_id, full_name, position, hire_date, salary, is_active) + VALUES (1, 'Иванов Алексей Петрович', 'Менеджер по продажам', '2023-01-15', 50000.00, 1) + """) + + # Добавляем пользователя + cursor.execute(""" + INSERT OR IGNORE INTO employees (employee_id, full_name, position, hire_date, salary, is_active) + VALUES (2, 'Петрова Мария Сергеевна', 'Аналитик', '2023-02-20', 45000.00, 1) + """) + + # Добавляем партнеров + partners_data = [ + (1, "ООО", "ООО «СтройГрад»", "г. Москва, ул. Ленина, 10", "770123456789", "Иван Петров", "+79001112233", "buildgrad@example.com", 4.5, "Москва, СПб", 1500000.00, 5.0), + (2, "ИП", "ИП Сидоров А.В.", "г. Казань, пр. Победы, 5", "165432109876", "Андрей Сидоров", "+79054445566", "sidorov@example.com", 4.2, "Казань", 800000.00, 3.0), + (3, "ТОО", "Торговый дом «Полимер+»", "г. Екатеринбург, ул. Мира, 22", "667890123456", "Елена Кузнецова", "+79107778899", "polymer@example.com", 4.8, "Екатеринбург, Челябинск", 2500000.00, 7.0), + ] + + for p in partners_data: + cursor.execute(""" + INSERT OR IGNORE INTO partners + (partner_id, partner_type, company_name, legal_address, inn, director_name, phone, email, rating, sales_locations, total_sales, discount_rate) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, p) + + # Добавляем поставщиков + suppliers_data = [ + (1, "ООО", "ООО «Сырье-Про»", "770987654321", "Петр Васильев", "+79012345678", "syrie@example.com", 4.7, "Древесина, клей, ламинация"), + (2, "ИП", "ИП Колесников С.И.", "163218765432", "Сергей Колесников", "+79087654321", "kolesnikov@example.com", 4.3, "ПВХ, пластификаторы"), + ] + + for s in suppliers_data: + cursor.execute(""" + INSERT OR IGNORE INTO suppliers + (supplier_id, supplier_type, company_name, inn, contact_person, phone, email, rating, supplied_materials) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, s) + + # Добавляем материалы + materials_data = [ + (1, "Древесина", "Дубовая доска", 1, 1.0, "м³", "Дубовая доска высшего сорта", 15000.00, 100.5, 20.0), + (2, "Клей", "Клей для ламината", 1, 25.0, "кг", "Водостойкий клей", 450.00, 500.0, 50.0), + (3, "ПВХ", "ПВХ пленка", 2, 50.0, "м²", "Декоративная ПВХ пленка", 320.00, 800.0, 100.0), + ] + + for m in materials_data: + cursor.execute(""" + INSERT OR IGNORE INTO materials + (material_id, material_type, material_name, supplier_id, package_quantity, unit_of_measure, description, cost_per_unit, current_stock, min_stock_level) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, m) + + # Добавляем продукцию + products_data = [ + (1, "LAM-001", "Ламинат", "Ламинат Quick-Step Classic", "Ламинат 32 класса, толщина 8мм", 1250.00, 1.2, 0.2, 0.08, 8.5, 9.2, "STD-045", 3, 850.00, 1, 2), + (2, "LAM-002", "Ламинат", "Ламинат Classen Premium", "Ламинат 33 класса, толщина 10мм", 1450.00, 1.3, 0.2, 0.09, 9.8, 10.5, "STD-048", 4, 950.00, 1, 2), + (3, "PL-001", "Плинтус", "Плинтус ПВХ белый", "Плинтус ПВХ 60мм, длина 2.5м", 350.00, 2.5, 0.06, 0.04, 0.45, 0.55, "STD-012", 1, 220.00, 2, 1), + ] + + for p in products_data: + cursor.execute(""" + INSERT OR IGNORE INTO products + (product_id, article_number, product_type, product_name, description, min_partner_price, package_length, package_width, package_height, net_weight, gross_weight, standard_number, production_time_days, cost_price, workshop_number, required_workers) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, p) + + # Добавляем связь продукции с материалами + product_materials_data = [ + (1, 1, 1, 0.05), # Ламинат Quick-Step -> Дубовая доска + (2, 1, 2, 0.8), # Ламинат Quick-Step -> Клей + (3, 1, 3, 1.2), # Ламинат Quick-Step -> ПВХ пленка + (4, 2, 1, 0.06), # Ламинат Classen -> Дубовая доска + (5, 2, 2, 1.0), # Ламинат Classen -> Клей + (6, 2, 3, 1.5), # Ламинат Classen -> ПВХ пленка + (7, 3, 3, 0.3), # Плинтус -> ПВХ пленка + ] + + for pm in product_materials_data: + cursor.execute(""" + INSERT OR IGNORE INTO product_materials (product_material_id, product_id, material_id, material_quantity) + VALUES (?, ?, ?, ?) + """, pm) + + # Добавляем запасы готовой продукции + finished_goods_data = [ + (1, 1, 500.0, 0, 50.0), + (2, 2, 300.0, 0, 30.0), + (3, 3, 1000.0, 0, 100.0), + ] + + for fg in finished_goods_data: + cursor.execute(""" + INSERT OR IGNORE INTO finished_goods_stock (stock_id, product_id, current_stock, reserved_stock, min_stock_level) + VALUES (?, ?, ?, ?, ?) + """, fg) + + # Добавляем тестовые заявки + orders_data = [ + (1, 1, 1, '2024-01-15', 'COMPLETED', 185000.00, 9250.00, 175750.00, 50000.00, '2024-01-16', '2024-01-25', '2024-01-20', '2024-01-22', 'Самовывоз', '', 'Первый заказ'), + (2, 2, 1, '2024-02-10', 'IN_PRODUCTION', 120000.00, 3600.00, 116400.00, 30000.00, '2024-02-11', None, '2024-02-25', None, 'Доставка курьером', 'г. Казань, пр. Победы, 5', 'Срочный заказ'), + (3, 3, 1, '2024-03-01', 'WAITING_PREPAYMENT', 250000.00, 17500.00, 232500.00, 0, None, None, '2024-03-20', None, 'Доставка транспортной компанией', 'г. Екатеринбург, ул. Мира, 22', 'Крупный опт'), + ] + + for order in orders_data: + cursor.execute(""" + INSERT OR IGNORE INTO orders + (order_id, partner_id, manager_id, order_date, status, total_amount, discount_amount, + final_amount, prepayment_amount, prepayment_date, full_payment_date, + expected_production_date, actual_production_date, delivery_method, delivery_address, notes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, order) + + # Добавляем позиции заказов + order_items_data = [ + (1, 1, 1, 100.0, 1250.00, 125000.00, 850.00), + (2, 1, 3, 200.0, 300.00, 60000.00, 220.00), + (3, 2, 2, 60.0, 1450.00, 87000.00, 950.00), + (4, 2, 3, 110.0, 300.00, 33000.00, 220.00), + (5, 3, 1, 150.0, 1250.00, 187500.00, 850.00), + (6, 3, 2, 40.0, 1450.00, 58000.00, 950.00), + (7, 3, 3, 150.0, 300.00, 45000.00, 220.00), + ] + + for item in order_items_data: + cursor.execute(""" + INSERT OR IGNORE INTO order_items + (order_item_id, order_id, product_id, quantity, unit_price, total_price, production_cost) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, item) + + # Добавляем больше тестовых продаж для демонстрации скидок + sales_data = [ + (1, "Ламинат Quick-Step Classic", 50.0, '2024-01-20', 1250.00, 62500.00), + (1, "Плинтус ПВХ белый", 100.0, '2024-01-20', 300.00, 30000.00), + (1, "Ламинат Classen Premium", 30.0, '2024-02-15', 1450.00, 43500.00), + (2, "Ламинат Quick-Step Classic", 25.0, '2024-01-25', 1250.00, 31250.00), + (2, "Плинтус ПВХ белый", 50.0, '2024-02-10', 300.00, 15000.00), + (3, "Ламинат Classen Premium", 100.0, '2024-01-30', 1450.00, 145000.00), + (3, "Ламинат Quick-Step Classic", 80.0, '2024-02-20', 1250.00, 100000.00), + (3, "Плинтус ПВХ белый", 200.0, '2024-03-01', 300.00, 60000.00), + ] + + for sale in sales_data: + cursor.execute(""" + INSERT OR IGNORE INTO sales + (partner_id, product_name, quantity, sale_date, unit_price, total_amount) + VALUES (?, ?, ?, ?, ?, ?) + """, sale) + + +# === Диалог авторизации === +class AuthDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Авторизация - Мастер пол") + self.setFixedSize(350, 250) + self.setStyleSheet(f""" + QDialog {{ + background-color: {APP_STYLES['primary_bg']}; + font-family: {APP_STYLES['font_family']}; + }} + QPushButton {{ + background-color: {APP_STYLES['accent_color']}; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + font-weight: bold; + }} + QPushButton:hover {{ + background-color: #5AA870; + }} + QLineEdit, QComboBox {{ + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + }} + """) + + self.authenticated = False + self.user_id = None + self.user_name = None + self.user_role = None + + layout = QVBoxLayout() + + # Заголовок + title = QLabel("Вход в систему") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 18px; font-weight: bold; margin: 10px;") + layout.addWidget(title) + + # Поля ввода + form_layout = QFormLayout() + + self.role_combo = QComboBox() + self.role_combo.addItems(["Менеджер", "Пользователь"]) + form_layout.addRow("Роль:", self.role_combo) + + self.login_edit = QLineEdit() + self.login_edit.setPlaceholderText("Введите логин") + form_layout.addRow("Логин:", self.login_edit) + + self.pass_edit = QLineEdit() + self.pass_edit.setPlaceholderText("Введите пароль") + self.pass_edit.setEchoMode(QLineEdit.EchoMode.Password) + form_layout.addRow("Пароль:", self.pass_edit) + + layout.addLayout(form_layout) + + # Кнопки + btn_layout = QHBoxLayout() + self.login_btn = QPushButton("Войти") + self.login_btn.clicked.connect(self.login) + self.cancel_btn = QPushButton("Отмена") + self.cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(self.login_btn) + btn_layout.addWidget(self.cancel_btn) + layout.addLayout(btn_layout) + + # Подсказка + hint = QLabel("Менеджер: manager/pass123\nПользователь: user/user123") + hint.setAlignment(Qt.AlignmentFlag.AlignCenter) + hint.setStyleSheet("color: #666; font-size: 12px; margin-top: 10px;") + layout.addWidget(hint) + + self.setLayout(layout) + + def login(self): + role = self.role_combo.currentText() + login = self.login_edit.text().strip() + password = self.pass_edit.text() + + if role == "Менеджер": + if login == "manager" and password == "pass123": + self.authenticated = True + self.user_id = 1 + self.user_name = "Иванов Алексей Петрович" + self.user_role = "manager" + self.accept() + else: + QMessageBox.warning(self, "Ошибка", "Неверный логин или пароль менеджера!") + else: # Пользователь + if login == "user" and password == "user123": + self.authenticated = True + self.user_id = 2 + self.user_name = "Петрова Мария Сергеевна" + self.user_role = "user" + self.accept() + else: + QMessageBox.warning(self, "Ошибка", "Неверный логин или пароль пользователя!") + + +# === Базовый класс для диалогов === +class BaseDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setStyleSheet(f""" + QDialog {{ + background-color: {APP_STYLES['primary_bg']}; + font-family: {APP_STYLES['font_family']}; + }} + QPushButton {{ + background-color: {APP_STYLES['accent_color']}; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + font-weight: bold; + }} + QPushButton:hover {{ + background-color: #5AA870; + }} + QLineEdit, QTextEdit, QComboBox, QDateEdit, QSpinBox, QDoubleSpinBox {{ + padding: 6px; + border: 1px solid #ccc; + border-radius: 4px; + }} + QGroupBox {{ + font-weight: bold; + margin-top: 10px; + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + }} + """) + + +# === Диалог партнера === +class PartnerDialog(BaseDialog): + def __init__(self, partner_data=None, parent=None): + super().__init__(parent) + self.partner_data = partner_data + title = "Добавить партнёра" if not partner_data else "Редактировать партнёра" + self.setWindowTitle(title) + self.setFixedSize(500, 550) + + layout = QVBoxLayout() + + # Основные данные + form_group = QGroupBox("Данные партнёра") + form_layout = QFormLayout() + + self.fields = { + "type": QComboBox(), + "name": QLineEdit(), + "address": QTextEdit(), + "inn": QLineEdit(), + "director": QLineEdit(), + "phone": QLineEdit(), + "email": QLineEdit(), + "rating": QDoubleSpinBox(), + "locations": QTextEdit(), + } + + # Настройка полей + self.fields["type"].addItems(["ООО", "ИП", "ТОО", "ЗАО", "ОАО", "Иное"]) + + inn_validator = QRegularExpressionValidator(QRegularExpression(r"^\d{10,12}$")) + self.fields["inn"].setValidator(inn_validator) + + self.fields["rating"].setRange(0.0, 5.0) + self.fields["rating"].setDecimals(2) + self.fields["rating"].setSingleStep(0.1) + + self.fields["address"].setMaximumHeight(70) + self.fields["locations"].setMaximumHeight(70) + + # Добавление полей в форму + form_layout.addRow("Тип партнёра *:", self.fields["type"]) + form_layout.addRow("Название компании *:", self.fields["name"]) + form_layout.addRow("Юридический адрес:", self.fields["address"]) + form_layout.addRow("ИНН *:", self.fields["inn"]) + form_layout.addRow("ФИО директора:", self.fields["director"]) + form_layout.addRow("Телефон:", self.fields["phone"]) + form_layout.addRow("Email:", self.fields["email"]) + form_layout.addRow("Рейтинг (0-5):", self.fields["rating"]) + form_layout.addRow("Места продаж:", self.fields["locations"]) + + form_group.setLayout(form_layout) + layout.addWidget(form_group) + + # Кнопки + btn_layout = QHBoxLayout() + self.save_btn = QPushButton("Сохранить") + self.save_btn.clicked.connect(self.accept) + self.cancel_btn = QPushButton("Отмена") + self.cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(self.save_btn) + btn_layout.addWidget(self.cancel_btn) + layout.addLayout(btn_layout) + + self.setLayout(layout) + + if partner_data: + self.load_data(partner_data) + + def load_data(self, data): + self.fields["type"].setCurrentText(data.get("partner_type") or "") + self.fields["name"].setText(data.get("company_name") or "") + self.fields["address"].setPlainText(data.get("legal_address") or "") + self.fields["inn"].setText(data.get("inn") or "") + self.fields["director"].setText(data.get("director_name") or "") + self.fields["phone"].setText(data.get("phone") or "") + self.fields["email"].setText(data.get("email") or "") + self.fields["rating"].setValue(float(data.get("rating") or 0.0)) + self.fields["locations"].setPlainText(data.get("sales_locations") or "") + + def get_data(self): + return { + "partner_type": self.fields["type"].currentText(), + "company_name": self.fields["name"].text().strip(), + "legal_address": self.fields["address"].toPlainText().strip() or None, + "inn": self.fields["inn"].text().strip(), + "director_name": self.fields["director"].text().strip() or None, + "phone": self.fields["phone"].text().strip() or None, + "email": self.fields["email"].text().strip() or None, + "rating": self.fields["rating"].value(), + "sales_locations": self.fields["locations"].toPlainText().strip() or None, + } + + def validate(self): + if not self.fields["name"].text().strip(): + QMessageBox.warning(self, "Ошибка", "Поле «Название компании» обязательно.") + return False + if not self.fields["inn"].text().strip(): + QMessageBox.warning(self, "Ошибка", "Поле «ИНН» обязательно.") + return False + if not self.fields["inn"].hasAcceptableInput(): + QMessageBox.warning(self, "Ошибка", "ИНН должен содержать 10 или 12 цифр.") + return False + return True + + def accept(self): + if self.validate(): + super().accept() + + +# === Диалог заказа === +class OrderDialog(BaseDialog): + def __init__(self, order_data=None, parent=None): + super().__init__(parent) + self.order_data = order_data + self.order_items = [] + + title = "Создать заявку" if not order_data else "Редактировать заявку" + self.setWindowTitle(title) + self.setMinimumSize(700, 600) + + layout = QVBoxLayout() + + # Основные данные заказа + form_group = QGroupBox("Данные заявки") + form_layout = QFormLayout() + + self.partner_combo = QComboBox() + self.status_combo = QComboBox() + self.order_date = QDateEdit() + self.expected_date = QDateEdit() + self.delivery_method = QComboBox() + self.delivery_address = QTextEdit() + self.notes = QTextEdit() + + # Настройка полей + self.status_combo.addItems(["NEW", "WAITING_PREPAYMENT", "IN_PRODUCTION", "READY_FOR_SHIPMENT", "SHIPPED", "COMPLETED", "CANCELLED"]) + self.order_date.setDate(QDate.currentDate()) + self.expected_date.setDate(QDate.currentDate().addDays(7)) + self.delivery_method.addItems(["Самовывоз", "Доставка курьером", "Доставка транспортной компанией"]) + + self.delivery_address.setMaximumHeight(60) + self.notes.setMaximumHeight(60) + + form_layout.addRow("Партнёр *:", self.partner_combo) + form_layout.addRow("Статус:", self.status_combo) + form_layout.addRow("Дата заявки:", self.order_date) + form_layout.addRow("Ожидаемая дата:", self.expected_date) + form_layout.addRow("Способ доставки:", self.delivery_method) + form_layout.addRow("Адрес доставки:", self.delivery_address) + form_layout.addRow("Примечания:", self.notes) + + form_group.setLayout(form_layout) + layout.addWidget(form_group) + + # Позиции заказа + items_group = QGroupBox("Позиции заказа") + items_layout = QVBoxLayout() + + # Таблица позиций + self.items_table = QTableWidget() + self.items_table.setColumnCount(5) + self.items_table.setHorizontalHeaderLabels(["Продукт", "Количество", "Цена", "Сумма", ""]) + self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + items_layout.addWidget(self.items_table) + + # Кнопки для позиций + items_btn_layout = QHBoxLayout() + self.add_item_btn = QPushButton("Добавить позицию") + self.add_item_btn.clicked.connect(self.add_order_item) + self.remove_item_btn = QPushButton("Удалить позицию") + self.remove_item_btn.clicked.connect(self.remove_order_item) + + items_btn_layout.addWidget(self.add_item_btn) + items_btn_layout.addWidget(self.remove_item_btn) + items_btn_layout.addStretch() + + items_layout.addLayout(items_btn_layout) + items_group.setLayout(items_layout) + layout.addWidget(items_group) + + # Итоги + totals_layout = QHBoxLayout() + self.total_label = QLabel("Итого: 0.00 руб.") + self.total_label.setStyleSheet("font-weight: bold; font-size: 14px;") + totals_layout.addStretch() + totals_layout.addWidget(self.total_label) + layout.addLayout(totals_layout) + + # Кнопки сохранения/отмены + btn_layout = QHBoxLayout() + self.save_btn = QPushButton("Сохранить заявку") + self.save_btn.clicked.connect(self.accept) + self.cancel_btn = QPushButton("Отмена") + self.cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(self.save_btn) + btn_layout.addWidget(self.cancel_btn) + layout.addLayout(btn_layout) + + self.setLayout(layout) + + self.load_partners() + + if order_data: + self.load_data(order_data) + + def load_partners(self): + """Загрузка списка партнеров в комбобокс""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT partner_id, company_name FROM partners ORDER BY company_name") + partners = cursor.fetchall() + conn.close() + + self.partner_combo.clear() + for partner_id, name in partners: + self.partner_combo.addItem(name, partner_id) + + def load_data(self, data): + """Загрузка данных заказа""" + # Загрузка основных данных + partner_index = self.partner_combo.findData(data.get("partner_id")) + if partner_index >= 0: + self.partner_combo.setCurrentIndex(partner_index) + + status_index = self.status_combo.findText(data.get("status", "NEW")) + if status_index >= 0: + self.status_combo.setCurrentIndex(status_index) + + order_date = QDate.fromString(data.get("order_date"), "yyyy-MM-dd") if data.get("order_date") else QDate.currentDate() + self.order_date.setDate(order_date) + + expected_date = QDate.fromString(data.get("expected_production_date"), "yyyy-MM-dd") if data.get("expected_production_date") else QDate.currentDate() + self.expected_date.setDate(expected_date) + + self.delivery_method.setCurrentText(data.get("delivery_method") or "") + self.delivery_address.setPlainText(data.get("delivery_address") or "") + self.notes.setPlainText(data.get("notes") or "") + + # Загрузка позиций заказа + self.load_order_items(data.get("order_id")) + + def load_order_items(self, order_id): + """Загрузка позиций заказа из БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT oi.product_id, p.product_name, oi.quantity, oi.unit_price, oi.total_price + FROM order_items oi + JOIN products p ON oi.product_id = p.product_id + WHERE oi.order_id = ? + """, (order_id,)) + + items = cursor.fetchall() + conn.close() + + self.order_items = [] + self.items_table.setRowCount(len(items)) + + for i, (product_id, product_name, quantity, unit_price, total_price) in enumerate(items): + self.items_table.setItem(i, 0, QTableWidgetItem(product_name)) + self.items_table.setItem(i, 1, QTableWidgetItem(str(quantity))) + self.items_table.setItem(i, 2, QTableWidgetItem(f"{unit_price:.2f}")) + self.items_table.setItem(i, 3, QTableWidgetItem(f"{total_price:.2f}")) + + remove_btn = QPushButton("Удалить") + remove_btn.clicked.connect(lambda checked, row=i: self.remove_specific_item(row)) + self.items_table.setCellWidget(i, 4, remove_btn) + + self.order_items.append({ + "product_id": product_id, + "product_name": product_name, + "quantity": quantity, + "unit_price": unit_price, + "total_price": total_price + }) + + self.update_totals() + + def add_order_item(self): + """Добавление новой позиции в заказ""" + dialog = OrderItemDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + item_data = dialog.get_data() + if item_data: + self.order_items.append(item_data) + self.update_items_table() + + def remove_order_item(self): + """Удаление выбранной позиции""" + current_row = self.items_table.currentRow() + if current_row >= 0 and current_row < len(self.order_items): + self.order_items.pop(current_row) + self.update_items_table() + + def remove_specific_item(self, row): + """Удаление конкретной позиции по кнопке""" + if 0 <= row < len(self.order_items): + self.order_items.pop(row) + self.update_items_table() + + def update_items_table(self): + """Обновление таблицы позиций""" + self.items_table.setRowCount(len(self.order_items)) + + for i, item in enumerate(self.order_items): + self.items_table.setItem(i, 0, QTableWidgetItem(item["product_name"])) + self.items_table.setItem(i, 1, QTableWidgetItem(str(item["quantity"]))) + self.items_table.setItem(i, 2, QTableWidgetItem(f"{item['unit_price']:.2f}")) + self.items_table.setItem(i, 3, QTableWidgetItem(f"{item['total_price']:.2f}")) + + remove_btn = QPushButton("Удалить") + remove_btn.clicked.connect(lambda checked, row=i: self.remove_specific_item(row)) + self.items_table.setCellWidget(i, 4, remove_btn) + + self.update_totals() + + def update_totals(self): + """Пересчет итоговой суммы""" + total = sum(item["total_price"] for item in self.order_items) + self.total_label.setText(f"Итого: {total:.2f} руб.") + + def get_data(self): + """Получение данных формы""" + data = { + "partner_id": self.partner_combo.currentData(), + "status": self.status_combo.currentText(), + "order_date": self.order_date.date().toString("yyyy-MM-dd"), + "expected_production_date": self.expected_date.date().toString("yyyy-MM-dd"), + "delivery_method": self.delivery_method.currentText(), + "delivery_address": self.delivery_address.toPlainText().strip(), + "notes": self.notes.toPlainText().strip(), + "total_amount": sum(item["total_price"] for item in self.order_items), + "order_items": self.order_items + } + + return data + + def validate(self): + """Валидация данных""" + if not self.partner_combo.currentData(): + QMessageBox.warning(self, "Ошибка", "Не выбран партнёр.") + return False + + if not self.order_items: + QMessageBox.warning(self, "Ошибка", "Добавьте хотя бы одну позицию в заказ.") + return False + + return True + + def accept(self): + if self.validate(): + super().accept() + + +# === Диалог позиции заказа === +class OrderItemDialog(BaseDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Добавить позицию") + self.setFixedSize(400, 300) + + layout = QVBoxLayout() + + form_layout = QFormLayout() + + self.product_combo = QComboBox() + self.quantity = QDoubleSpinBox() + self.unit_price = QDoubleSpinBox() + self.total_price = QLabel("0.00") + + # Настройка полей + self.quantity.setRange(0.1, 10000.0) + self.quantity.setDecimals(3) + self.quantity.setValue(1.0) + + self.unit_price.setRange(0.01, 100000.0) + self.unit_price.setDecimals(2) + + # Связывание сигналов для автоматического пересчета + self.quantity.valueChanged.connect(self.calculate_total) + self.unit_price.valueChanged.connect(self.calculate_total) + + form_layout.addRow("Продукт *:", self.product_combo) + form_layout.addRow("Количество *:", self.quantity) + form_layout.addRow("Цена за единицу *:", self.unit_price) + form_layout.addRow("Общая сумма:", self.total_price) + + layout.addLayout(form_layout) + + # Кнопки + btn_layout = QHBoxLayout() + self.add_btn = QPushButton("Добавить") + self.add_btn.clicked.connect(self.accept) + self.cancel_btn = QPushButton("Отмена") + self.cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(self.add_btn) + btn_layout.addWidget(self.cancel_btn) + layout.addLayout(btn_layout) + + self.setLayout(layout) + + self.load_products() + + def load_products(self): + """Загрузка списка продукции""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT product_id, product_name, min_partner_price FROM products WHERE is_active = 1") + products = cursor.fetchall() + conn.close() + + self.product_combo.clear() + for product_id, product_name, min_price in products: + self.product_combo.addItem(f"{product_name} ({min_price:.2f} руб.)", (product_id, min_price)) + + if self.product_combo.count() > 0: + self.product_combo.currentIndexChanged.connect(self.product_changed) + self.product_changed(0) # Установить цену для первого товара + + def product_changed(self, index): + """Обработчик изменения выбранного продукта""" + if index >= 0: + product_data = self.product_combo.currentData() + if product_data: + product_id, min_price = product_data + self.unit_price.setValue(float(min_price)) + + def calculate_total(self): + """Пересчет общей суммы""" + total = self.quantity.value() * self.unit_price.value() + self.total_price.setText(f"{total:.2f}") + + def get_data(self): + """Получение данных позиции""" + if self.product_combo.currentIndex() < 0: + return None + + product_data = self.product_combo.currentData() + if not product_data: + return None + + product_id, min_price = product_data + quantity = self.quantity.value() + unit_price = self.unit_price.value() + total_price = quantity * unit_price + + return { + "product_id": product_id, + "product_name": self.product_combo.currentText().split(' (')[0], # Извлекаем название без цены + "quantity": quantity, + "unit_price": unit_price, + "total_price": total_price + } + + def validate(self): + """Валидация данных""" + if self.product_combo.currentIndex() < 0: + QMessageBox.warning(self, "Ошибка", "Не выбран продукт.") + return False + + if self.quantity.value() <= 0: + QMessageBox.warning(self, "Ошибка", "Количество должно быть больше 0.") + return False + + if self.unit_price.value() <= 0: + QMessageBox.warning(self, "Ошибка", "Цена должна быть больше 0.") + return False + + return True + + def accept(self): + if self.validate(): + super().accept() + + +# === Основное окно приложения === +class MainWindow(QMainWindow): + def __init__(self, user_id, user_name, user_role): + super().__init__() + self.user_id = user_id + self.user_name = user_name + self.user_role = user_role + + role_display = "Менеджер" if user_role == "manager" else "Пользователь" + self.setWindowTitle(f"Мастер пол - Система управления ({role_display}: {user_name})") + self.setMinimumSize(1200, 700) + + # Установка стилей + self.setStyleSheet(f""" + QMainWindow {{ + background-color: {APP_STYLES['primary_bg']}; + font-family: {APP_STYLES['font_family']}; + }} + QTabWidget::pane {{ + border: 1px solid #C2C7CB; + background-color: {APP_STYLES['primary_bg']}; + }} + QTabBar::tab {{ + background-color: {APP_STYLES['secondary_bg']}; + border: 1px solid #C2C7CB; + padding: 8px 15px; + margin-right: 2px; + }} + QTabBar::tab:selected {{ + background-color: {APP_STYLES['accent_color']}; + color: white; + }} + QPushButton {{ + background-color: {APP_STYLES['accent_color']}; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + font-weight: bold; + }} + QPushButton:hover {{ + background-color: #5AA870; + }} + QPushButton:disabled {{ + background-color: #CCCCCC; + color: #666666; + }} + QTableWidget {{ + gridline-color: #D0D0D0; + selection-background-color: {APP_STYLES['accent_color']}; + }} + QHeaderView::section {{ + background-color: {APP_STYLES['secondary_bg']}; + padding: 5px; + border: 1px solid #D0D0D0; + font-weight: bold; + }} + """) + + self.init_ui() + self.load_initial_data() + + def init_ui(self): + """Инициализация пользовательского интерфейса""" + central_widget = QWidget() + self.setCentralWidget(central_widget) + + layout = QVBoxLayout() + + # Заголовок + header_layout = QHBoxLayout() + title = QLabel("Система управления производством «Мастер пол»") + title.setStyleSheet("font-size: 20px; font-weight: bold; color: #333;") + header_layout.addWidget(title) + header_layout.addStretch() + + user_label = QLabel(f"{'Менеджер' if self.user_role == 'manager' else 'Пользователь'}: {self.user_name}") + user_label.setStyleSheet("color: #666;") + header_layout.addWidget(user_label) + + layout.addLayout(header_layout) + + # Вкладки + self.tabs = QTabWidget() + + # Создаем вкладки + self.partners_tab = self.create_partners_tab() + self.orders_tab = self.create_orders_tab() + self.products_tab = self.create_products_tab() + self.employees_tab = self.create_employees_tab() + self.materials_tab = self.create_materials_tab() + + self.tabs.addTab(self.partners_tab, "Партнёры") + self.tabs.addTab(self.orders_tab, "Заявки") + self.tabs.addTab(self.products_tab, "Продукция") + self.tabs.addTab(self.employees_tab, "Сотрудники") + self.tabs.addTab(self.materials_tab, "Материалы") + + layout.addWidget(self.tabs) + central_widget.setLayout(layout) + + # Настройка прав доступа в зависимости от роли + self.setup_permissions() + + def setup_permissions(self): + """Настройка прав доступа в зависимости от роли пользователя""" + is_manager = self.user_role == "manager" + + # Партнеры + self.add_partner_btn.setEnabled(is_manager) + self.edit_partner_btn.setEnabled(is_manager) + self.delete_partner_btn.setEnabled(is_manager) + self.update_discounts_btn.setEnabled(is_manager) + + # Заявки + self.add_order_btn.setEnabled(is_manager) + self.edit_order_btn.setEnabled(is_manager) + self.update_status_btn.setEnabled(is_manager) + self.delete_order_btn.setEnabled(is_manager) + + # Продукция + self.add_product_btn.setEnabled(is_manager) + self.edit_product_btn.setEnabled(is_manager) + self.delete_product_btn.setEnabled(is_manager) + + # Сотрудники + self.add_employee_btn.setEnabled(is_manager) + self.edit_employee_btn.setEnabled(is_manager) + self.delete_employee_btn.setEnabled(is_manager) + + # Материалы + self.add_material_btn.setEnabled(is_manager) + self.edit_material_btn.setEnabled(is_manager) + self.delete_material_btn.setEnabled(is_manager) + + def create_partners_tab(self): + """Создание вкладки партнеров""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_partner_btn = QPushButton("➕ Добавить партнёра") + self.edit_partner_btn = QPushButton("✏️ Редактировать") + self.view_sales_btn = QPushButton("📊 История продаж") + self.delete_partner_btn = QPushButton("🗑 Удалить") + self.update_discounts_btn = QPushButton("🎯 Обновить скидки") # Новая кнопка + self.refresh_partners_btn = QPushButton("🔄 Обновить") + + self.add_partner_btn.clicked.connect(self.add_partner) + self.edit_partner_btn.clicked.connect(self.edit_partner) + self.view_sales_btn.clicked.connect(self.view_sales_history) + self.delete_partner_btn.clicked.connect(self.delete_partner) + self.update_discounts_btn.clicked.connect(self.update_partner_discounts) # Новый обработчик + self.refresh_partners_btn.clicked.connect(self.load_partners) + + control_layout.addWidget(self.add_partner_btn) + control_layout.addWidget(self.edit_partner_btn) + control_layout.addWidget(self.view_sales_btn) + control_layout.addWidget(self.delete_partner_btn) + control_layout.addWidget(self.update_discounts_btn) # Добавляем кнопку в layout + control_layout.addStretch() + control_layout.addWidget(self.refresh_partners_btn) + + layout.addLayout(control_layout) + + # Таблица партнеров + self.partners_table = QTableWidget() + self.partners_table.setColumnCount(10) + self.partners_table.setHorizontalHeaderLabels([ + "ID", "Тип", "Компания", "ИНН", "Директор", "Телефон", "Email", "Рейтинг", "Общие продажи", "Скидка %" + ]) + + # Настройка таблицы + header = self.partners_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(7, QHeaderView.ResizeMode.ResizeToContents) + + self.partners_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.partners_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.partners_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + # Двойной клик для редактирования + self.partners_table.doubleClicked.connect(self.edit_partner) + + layout.addWidget(self.partners_table) + widget.setLayout(layout) + + return widget + + def create_orders_tab(self): + """Создание вкладки заявок""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_order_btn = QPushButton("➕ Новая заявка") + self.edit_order_btn = QPushButton("✏️ Редактировать") + self.view_order_btn = QPushButton("👁 Просмотреть") + self.update_status_btn = QPushButton("🔄 Обновить статус") + self.delete_order_btn = QPushButton("🗑 Удалить") + self.refresh_orders_btn = QPushButton("🔄 Обновить") + + self.add_order_btn.clicked.connect(self.add_order) + self.edit_order_btn.clicked.connect(self.edit_order) + self.view_order_btn.clicked.connect(self.view_order) + self.update_status_btn.clicked.connect(self.update_order_status) + self.delete_order_btn.clicked.connect(self.delete_order) + self.refresh_orders_btn.clicked.connect(self.load_orders) + + control_layout.addWidget(self.add_order_btn) + control_layout.addWidget(self.edit_order_btn) + control_layout.addWidget(self.view_order_btn) + control_layout.addWidget(self.update_status_btn) + control_layout.addWidget(self.delete_order_btn) + control_layout.addStretch() + control_layout.addWidget(self.refresh_orders_btn) + + layout.addLayout(control_layout) + + # Таблица заявок + self.orders_table = QTableWidget() + self.orders_table.setColumnCount(8) + self.orders_table.setHorizontalHeaderLabels([ + "ID", "Партнёр", "Дата", "Статус", "Сумма", "Скидка", "Итог", "Менеджер" + ]) + + # Настройка таблицы + header = self.orders_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) + + self.orders_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.orders_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.orders_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + layout.addWidget(self.orders_table) + widget.setLayout(layout) + + return widget + + def create_products_tab(self): + """Создание вкладки продукции""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_product_btn = QPushButton("➕ Добавить продукт") + self.edit_product_btn = QPushButton("✏️ Редактировать") + self.view_materials_btn = QPushButton("📋 Состав продукции") + self.delete_product_btn = QPushButton("🗑 Удалить") + self.refresh_products_btn = QPushButton("🔄 Обновить") + + self.add_product_btn.clicked.connect(self.add_product) + self.edit_product_btn.clicked.connect(self.edit_product) + self.view_materials_btn.clicked.connect(self.view_product_materials) + self.delete_product_btn.clicked.connect(self.delete_product) + self.refresh_products_btn.clicked.connect(self.load_products) + + control_layout.addWidget(self.add_product_btn) + control_layout.addWidget(self.edit_product_btn) + control_layout.addWidget(self.view_materials_btn) + control_layout.addWidget(self.delete_product_btn) + control_layout.addStretch() + control_layout.addWidget(self.refresh_products_btn) + + layout.addLayout(control_layout) + + # Таблица продукции + self.products_table = QTableWidget() + self.products_table.setColumnCount(10) + self.products_table.setHorizontalHeaderLabels([ + "ID", "Артикул", "Тип", "Наименование", "Мин. цена", "Вес нетто", "Вес брутто", "Время пр-ва", "Себестоимость", "Цех" + ]) + + # Настройка таблицы + header = self.products_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) + + self.products_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.products_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.products_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + layout.addWidget(self.products_table) + widget.setLayout(layout) + + return widget + + def create_employees_tab(self): + """Создание вкладки сотрудников""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_employee_btn = QPushButton("➕ Добавить сотрудника") + self.edit_employee_btn = QPushButton("✏️ Редактировать") + self.view_access_btn = QPushButton("🔧 Доступ к оборудованию") + self.delete_employee_btn = QPushButton("🗑 Удалить") + self.refresh_employees_btn = QPushButton("🔄 Обновить") + + self.add_employee_btn.clicked.connect(self.add_employee) + self.edit_employee_btn.clicked.connect(self.edit_employee) + self.view_access_btn.clicked.connect(self.view_equipment_access) + self.delete_employee_btn.clicked.connect(self.delete_employee) + self.refresh_employees_btn.clicked.connect(self.load_employees) + + control_layout.addWidget(self.add_employee_btn) + control_layout.addWidget(self.edit_employee_btn) + control_layout.addWidget(self.view_access_btn) + control_layout.addWidget(self.delete_employee_btn) + control_layout.addStretch() + control_layout.addWidget(self.refresh_employees_btn) + + layout.addLayout(control_layout) + + # Таблица сотрудников + self.employees_table = QTableWidget() + self.employees_table.setColumnCount(8) + self.employees_table.setHorizontalHeaderLabels([ + "ID", "ФИО", "Должность", "Дата найма", "Зарплата", "Дата рождения", "Семья", "Статус" + ]) + + # Настройка таблицы + header = self.employees_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + + self.employees_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.employees_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.employees_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + layout.addWidget(self.employees_table) + widget.setLayout(layout) + + return widget + + def create_materials_tab(self): + """Создание вкладки материалов""" + widget = QWidget() + layout = QVBoxLayout() + + # Панель управления + control_layout = QHBoxLayout() + + self.add_material_btn = QPushButton("➕ Добавить материал") + self.edit_material_btn = QPushButton("✏️ Редактировать") + self.view_stock_btn = QPushButton("📊 История запасов") + self.delete_material_btn = QPushButton("🗑 Удалить") + self.refresh_materials_btn = QPushButton("🔄 Обновить") + + self.add_material_btn.clicked.connect(self.add_material) + self.edit_material_btn.clicked.connect(self.edit_material) + self.view_stock_btn.clicked.connect(self.view_stock_history) + self.delete_material_btn.clicked.connect(self.delete_material) + self.refresh_materials_btn.clicked.connect(self.load_materials) + + control_layout.addWidget(self.add_material_btn) + control_layout.addWidget(self.edit_material_btn) + control_layout.addWidget(self.view_stock_btn) + control_layout.addWidget(self.delete_material_btn) + control_layout.addStretch() + control_layout.addWidget(self.refresh_materials_btn) + + layout.addLayout(control_layout) + + # Таблица материалов + self.materials_table = QTableWidget() + self.materials_table.setColumnCount(9) + self.materials_table.setHorizontalHeaderLabels([ + "ID", "Тип", "Наименование", "Поставщик", "Ед. изм.", "Цена", "Текущий запас", "Мин. запас", "Описание" + ]) + + # Настройка таблицы + header = self.materials_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) + + self.materials_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.materials_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + self.materials_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + layout.addWidget(self.materials_table) + widget.setLayout(layout) + + return widget + + def load_initial_data(self): + """Загрузка начальных данных во все таблицы""" + self.load_partners() + self.load_orders() + self.load_products() + self.load_employees() + self.load_materials() + + def load_partners(self): + """Загрузка данных партнеров""" + self.partners_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT partner_id, partner_type, company_name, inn, director_name, + phone, email, rating, total_sales, discount_rate + FROM partners + ORDER BY company_name + """) + rows = cursor.fetchall() + conn.close() + + self.partners_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j in [7, 8, 9] and val is not None: # рейтинг, продажи, скидка + if j == 7: # рейтинг + item = QTableWidgetItem(f"{float(val):.2f}") + elif j == 8: # продажи + item = QTableWidgetItem(f"{float(val):.2f}") + else: # скидка + item = QTableWidgetItem(f"{float(val):.1f}%") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.partners_table.setItem(i, j, item) + + def load_orders(self): + """Загрузка данных заявок""" + self.orders_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT o.order_id, p.company_name, o.order_date, o.status, + o.total_amount, o.discount_amount, o.final_amount, e.full_name + FROM orders o + JOIN partners p ON o.partner_id = p.partner_id + JOIN employees e ON o.manager_id = e.employee_id + ORDER BY o.order_date DESC + """) + rows = cursor.fetchall() + conn.close() + + self.orders_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j in [4, 5, 6] and val is not None: # суммы + item = QTableWidgetItem(f"{float(val):.2f}") + elif j == 3: # статус + status_text = { + 'NEW': 'Новая', + 'WAITING_PREPAYMENT': 'Ожидает предоплаты', + 'IN_PRODUCTION': 'В производстве', + 'READY_FOR_SHIPMENT': 'Готов к отгрузке', + 'SHIPPED': 'Отгружен', + 'COMPLETED': 'Завершен', + 'CANCELLED': 'Отменен' + }.get(val, val) + item = QTableWidgetItem(status_text) + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.orders_table.setItem(i, j, item) + + def load_products(self): + """Загрузка данных продукции""" + self.products_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT product_id, article_number, product_type, product_name, + min_partner_price, net_weight, gross_weight, production_time_days, + cost_price, workshop_number + FROM products + WHERE is_active = 1 + ORDER BY product_type, product_name + """) + rows = cursor.fetchall() + conn.close() + + self.products_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j in [4, 5, 6, 8] and val is not None: # цены и веса + item = QTableWidgetItem(f"{float(val):.2f}") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.products_table.setItem(i, j, item) + + def load_employees(self): + """Загрузка данных сотрудников""" + self.employees_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT employee_id, full_name, position, hire_date, salary, + birth_date, has_family, is_active + FROM employees + ORDER BY full_name + """) + rows = cursor.fetchall() + conn.close() + + self.employees_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j == 4 and val is not None: # зарплата + item = QTableWidgetItem(f"{float(val):.2f}") + elif j == 6: # семья + item = QTableWidgetItem("Да" if val else "Нет") + elif j == 7: # статус + item = QTableWidgetItem("Активен" if val else "Неактивен") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.employees_table.setItem(i, j, item) + + def load_materials(self): + """Загрузка данных материалов""" + self.materials_table.setRowCount(0) + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute(""" + SELECT m.material_id, m.material_type, m.material_name, + s.company_name, m.unit_of_measure, m.cost_per_unit, + m.current_stock, m.min_stock_level, m.description + FROM materials m + LEFT JOIN suppliers s ON m.supplier_id = s.supplier_id + ORDER BY m.material_type, m.material_name + """) + rows = cursor.fetchall() + conn.close() + + self.materials_table.setRowCount(len(rows)) + for i, row in enumerate(rows): + for j, val in enumerate(row): + if j in [5, 6, 7] and val is not None: # цена и запасы + item = QTableWidgetItem(f"{float(val):.2f}") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + self.materials_table.setItem(i, j, item) + + def get_selected_partner_id(self): + """Получение ID выбранного партнера""" + selected = self.partners_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите партнёра в таблице.") + return None + + row = selected[0].row() + item = self.partners_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + def get_selected_order_id(self): + """Получение ID выбранной заявки""" + selected = self.orders_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите заявку в таблице.") + return None + + row = selected[0].row() + item = self.orders_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + def get_selected_product_id(self): + """Получение ID выбранной продукции""" + selected = self.products_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите продукт в таблице.") + return None + + row = selected[0].row() + item = self.products_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + def get_selected_employee_id(self): + """Получение ID выбранного сотрудника""" + selected = self.employees_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите сотрудника в таблице.") + return None + + row = selected[0].row() + item = self.employees_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + def get_selected_material_id(self): + """Получение ID выбранного материала""" + selected = self.materials_table.selectedItems() + if not selected: + QMessageBox.warning(self, "Внимание", "Выберите материал в таблице.") + return None + + row = selected[0].row() + item = self.materials_table.item(row, 0) + return int(item.text()) if item and item.text() else None + + # === Методы для партнеров === + def add_partner(self): + """Добавление нового партнера""" + dialog = PartnerDialog() + if dialog.exec() == QDialog.DialogCode.Accepted: + data = dialog.get_data() + self.save_partner_to_db(data) + + def edit_partner(self): + """Редактирование выбранного партнера""" + partner_id = self.get_selected_partner_id() + if not partner_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT * FROM partners WHERE partner_id = ?", (partner_id,)) + row = cursor.fetchone() + conn.close() + + if not row: + QMessageBox.warning(self, "Ошибка", "Партнёр не найден.") + return + + # Преобразование в словарь + columns = [description[0] for description in cursor.description] + partner_data = dict(zip(columns, row)) + + dialog = PartnerDialog(partner_data) + if dialog.exec() == QDialog.DialogCode.Accepted: + data = dialog.get_data() + data["partner_id"] = partner_id + self.update_partner_in_db(data) + + def view_sales_history(self): + """Просмотр истории продаж партнера""" + partner_id = self.get_selected_partner_id() + if not partner_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + # Получение названия компании + cursor.execute("SELECT company_name FROM partners WHERE partner_id = ?", (partner_id,)) + partner_name = cursor.fetchone()[0] + + # Получение истории продаж + cursor.execute(""" + SELECT product_name, quantity, unit_price, total_amount, sale_date + FROM sales + WHERE partner_id = ? + ORDER BY sale_date DESC + """, (partner_id,)) + sales = cursor.fetchall() + conn.close() + + # Создание диалога для отображения истории + dialog = BaseDialog(self) + dialog.setWindowTitle(f"История продаж: {partner_name}") + dialog.setFixedSize(600, 400) + + layout = QVBoxLayout() + + # Таблица продаж + table = QTableWidget() + table.setColumnCount(5) + table.setHorizontalHeaderLabels(["Продукт", "Количество", "Цена", "Сумма", "Дата"]) + table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + + table.setRowCount(len(sales)) + for i, sale in enumerate(sales): + for j, val in enumerate(sale): + if j in [1, 2, 3] and val is not None: # количества и цены + if j == 1: # количество + item = QTableWidgetItem(f"{float(val):.3f}") + else: # цены + item = QTableWidgetItem(f"{float(val):.2f}") + else: + item = QTableWidgetItem(str(val) if val is not None else "") + table.setItem(i, j, item) + + layout.addWidget(table) + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.setLayout(layout) + dialog.exec() + + def delete_partner(self): + """Удаление выбранного партнера""" + partner_id = self.get_selected_partner_id() + if not partner_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить этого партнёра?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("DELETE FROM partners WHERE partner_id = ?", (partner_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Партнёр удалён.") + self.load_partners() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить партнёра: {e}") + finally: + conn.close() + + def update_partner_discounts(self): + """Обновление скидок для всех партнеров""" + if self.user_role != "manager": + QMessageBox.warning(self, "Ошибка", "Только менеджер может обновлять скидки.") + return + + reply = QMessageBox.question( + self, "Подтверждение", + "Обновить скидки для всех партнеров на основе текущих продаж и рейтингов?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + try: + updated_count = update_all_partners_discounts() + QMessageBox.information( + self, "Успех", + f"Скидки обновлены для {updated_count} партнеров.\n" + f"Скидки рассчитываются по формуле:\n" + f"- Рейтинг × 2% (макс. 10%)\n" + f"- Бонус за количество продаж (макс. 15%)\n" + f"- Бонус за объем продаж (до 2%)\n" + f"- Максимальная скидка: 25%" + ) + self.load_partners() + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось обновить скидки: {e}") + + def save_partner_to_db(self, data): + """Сохранение нового партнера в БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT INTO partners + (partner_type, company_name, legal_address, inn, director_name, phone, email, rating, sales_locations) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + data["partner_type"], + data["company_name"], + data["legal_address"], + data["inn"], + data["director_name"], + data["phone"], + data["email"], + data["rating"], + data["sales_locations"] + )) + conn.commit() + QMessageBox.information(self, "Успех", "Партнёр добавлен.") + self.load_partners() + except sqlite3.IntegrityError: + QMessageBox.critical(self, "Ошибка", "Партнёр с таким ИНН уже существует.") + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось добавить партнёра: {e}") + finally: + conn.close() + + def update_partner_in_db(self, data): + """Обновление данных партнера в БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + cursor.execute(""" + UPDATE partners SET + partner_type = ?, company_name = ?, legal_address = ?, inn = ?, + director_name = ?, phone = ?, email = ?, rating = ?, sales_locations = ? + WHERE partner_id = ? + """, ( + data["partner_type"], + data["company_name"], + data["legal_address"], + data["inn"], + data["director_name"], + data["phone"], + data["email"], + data["rating"], + data["sales_locations"], + data["partner_id"] + )) + conn.commit() + QMessageBox.information(self, "Успех", "Данные партнёра обновлены.") + self.load_partners() + except sqlite3.IntegrityError: + QMessageBox.critical(self, "Ошибка", "Партнёр с таким ИНН уже существует.") + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось обновить данные: {e}") + finally: + conn.close() + + # === Методы для заявок === + def add_order(self): + """Создание новой заявки""" + dialog = OrderDialog() + if dialog.exec() == QDialog.DialogCode.Accepted: + data = dialog.get_data() + self.save_order_to_db(data) + + def edit_order(self): + """Редактирование заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)) + row = cursor.fetchone() + conn.close() + + if not row: + QMessageBox.warning(self, "Ошибка", "Заявка не найдена.") + return + + # Преобразование в словарь + columns = [description[0] for description in cursor.description] + order_data = dict(zip(columns, row)) + + dialog = OrderDialog(order_data) + if dialog.exec() == QDialog.DialogCode.Accepted: + data = dialog.get_data() + data["order_id"] = order_id + self.update_order_in_db(data) + + def view_order(self): + """Просмотр деталей заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + # Получение основной информации о заявке + cursor.execute(""" + SELECT o.*, p.company_name, e.full_name + FROM orders o + JOIN partners p ON o.partner_id = p.partner_id + JOIN employees e ON o.manager_id = e.employee_id + WHERE o.order_id = ? + """, (order_id,)) + order = cursor.fetchone() + + if not order: + QMessageBox.warning(self, "Ошибка", "Заявка не найдена.") + conn.close() + return + + # Получение позиций заявки + cursor.execute(""" + SELECT p.product_name, oi.quantity, oi.unit_price, oi.total_price + FROM order_items oi + JOIN products p ON oi.product_id = p.product_id + WHERE oi.order_id = ? + """, (order_id,)) + items = cursor.fetchall() + conn.close() + + # Создание диалога для просмотра + dialog = BaseDialog(self) + dialog.setWindowTitle(f"Детали заявки #{order_id}") + dialog.setFixedSize(600, 500) + + layout = QVBoxLayout() + + # Основная информация + info_group = QGroupBox("Информация о заявке") + info_layout = QFormLayout() + + info_layout.addRow("Номер заявки:", QLabel(str(order[0]))) + info_layout.addRow("Партнёр:", QLabel(order[16])) # company_name + info_layout.addRow("Менеджер:", QLabel(order[17])) # full_name + info_layout.addRow("Дата заявки:", QLabel(order[3])) + info_layout.addRow("Статус:", QLabel(order[4])) + info_layout.addRow("Общая сумма:", QLabel(f"{order[5]:.2f} руб.")) + info_layout.addRow("Скидка:", QLabel(f"{order[6]:.2f} руб.")) + info_layout.addRow("Итоговая сумма:", QLabel(f"{order[7]:.2f} руб.")) + + info_group.setLayout(info_layout) + layout.addWidget(info_group) + + # Позиции заявки + items_group = QGroupBox("Позиции заявки") + items_layout = QVBoxLayout() + + table = QTableWidget() + table.setColumnCount(4) + table.setHorizontalHeaderLabels(["Продукт", "Количество", "Цена", "Сумма"]) + table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + + table.setRowCount(len(items)) + total = 0 + for i, item in enumerate(items): + for j, val in enumerate(item): + if j in [1, 2, 3] and val is not None: + item_text = f"{float(val):.2f}" if j in [2, 3] else f"{float(val):.3f}" + table.setItem(i, j, QTableWidgetItem(item_text)) + else: + table.setItem(i, j, QTableWidgetItem(str(val))) + total += float(item[3]) + + items_layout.addWidget(table) + items_group.setLayout(items_layout) + layout.addWidget(items_group) + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.setLayout(layout) + dialog.exec() + + def update_order_status(self): + """Обновление статуса заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("SELECT status FROM orders WHERE order_id = ?", (order_id,)) + current_status = cursor.fetchone()[0] + conn.close() + + dialog = BaseDialog(self) + dialog.setWindowTitle("Обновление статуса заявки") + dialog.setFixedSize(300, 150) + + layout = QVBoxLayout() + + layout.addWidget(QLabel("Текущий статус: " + current_status)) + + status_combo = QComboBox() + status_combo.addItems(["NEW", "WAITING_PREPAYMENT", "IN_PRODUCTION", "READY_FOR_SHIPMENT", "SHIPPED", "COMPLETED", "CANCELLED"]) + status_combo.setCurrentText(current_status) + layout.addWidget(QLabel("Новый статус:")) + layout.addWidget(status_combo) + + btn_layout = QHBoxLayout() + save_btn = QPushButton("Сохранить") + cancel_btn = QPushButton("Отмена") + + btn_layout.addWidget(save_btn) + btn_layout.addWidget(cancel_btn) + layout.addLayout(btn_layout) + + dialog.setLayout(layout) + + def save_status(): + new_status = status_combo.currentText() + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + cursor.execute("UPDATE orders SET status = ? WHERE order_id = ?", (new_status, order_id)) + conn.commit() + conn.close() + QMessageBox.information(self, "Успех", "Статус заявки обновлен.") + self.load_orders() + dialog.accept() + + save_btn.clicked.connect(save_status) + cancel_btn.clicked.connect(dialog.reject) + + dialog.exec() + + def delete_order(self): + """Удаление заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить эту заявку?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("DELETE FROM orders WHERE order_id = ?", (order_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Заявка удалена.") + self.load_orders() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить заявку: {e}") + finally: + conn.close() + + def save_order_to_db(self, data): + """Сохранение заявки в БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + # Вставка основной информации о заявке + cursor.execute(""" + INSERT INTO orders + (partner_id, manager_id, order_date, status, expected_production_date, + delivery_method, delivery_address, notes, total_amount, final_amount) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + data["partner_id"], + self.user_id, # ID текущего менеджера + data["order_date"], + data["status"], + data["expected_production_date"], + data["delivery_method"], + data["delivery_address"], + data["notes"], + data["total_amount"], + data["total_amount"] # final_amount = total_amount (без скидки в демо) + )) + + order_id = cursor.lastrowid + + # Вставка позиций заявки + for item in data["order_items"]: + cursor.execute(""" + INSERT INTO order_items + (order_id, product_id, quantity, unit_price, total_price) + VALUES (?, ?, ?, ?, ?) + """, ( + order_id, + item["product_id"], + item["quantity"], + item["unit_price"], + item["total_price"] + )) + + conn.commit() + QMessageBox.information(self, "Успех", "Заявка создана.") + self.load_orders() + + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось создать заявку: {e}") + finally: + conn.close() + + def update_order_in_db(self, data): + """Обновление заявки в БД""" + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + + try: + # Обновление основной информации о заявке + cursor.execute(""" + UPDATE orders SET + partner_id = ?, status = ?, order_date = ?, expected_production_date = ?, + delivery_method = ?, delivery_address = ?, notes = ?, total_amount = ?, final_amount = ? + WHERE order_id = ? + """, ( + data["partner_id"], + data["status"], + data["order_date"], + data["expected_production_date"], + data["delivery_method"], + data["delivery_address"], + data["notes"], + data["total_amount"], + data["total_amount"], + data["order_id"] + )) + + # Удаляем старые позиции и добавляем новые + cursor.execute("DELETE FROM order_items WHERE order_id = ?", (data["order_id"],)) + + for item in data["order_items"]: + cursor.execute(""" + INSERT INTO order_items + (order_id, product_id, quantity, unit_price, total_price) + VALUES (?, ?, ?, ?, ?) + """, ( + data["order_id"], + item["product_id"], + item["quantity"], + item["unit_price"], + item["total_price"] + )) + + conn.commit() + QMessageBox.information(self, "Успех", "Заявка обновлена.") + self.load_orders() + + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось обновить заявку: {e}") + finally: + conn.close() + + # === Методы для продукции (заглушки) === + def add_product(self): + QMessageBox.information(self, "Информация", "Добавление продукции будет реализовано в полной версии.") + + def edit_product(self): + QMessageBox.information(self, "Информация", "Редактирование продукции будет реализовано в полной версии.") + + def view_product_materials(self): + QMessageBox.information(self, "Информация", "Просмотр состава продукции будет реализовано в полной версии.") + + def delete_product(self): + product_id = self.get_selected_product_id() + if not product_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить этот продукт?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("UPDATE products SET is_active = 0 WHERE product_id = ?", (product_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Продукт удален.") + self.load_products() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить продукт: {e}") + finally: + conn.close() + + # === Методы для сотрудников (заглушки) === + def add_employee(self): + QMessageBox.information(self, "Информация", "Добавление сотрудника будет реализовано в полной версии.") + + def edit_employee(self): + QMessageBox.information(self, "Информация", "Редактирование сотрудника будет реализовано в полной версии.") + + def view_equipment_access(self): + QMessageBox.information(self, "Информация", "Просмотр доступа к оборудованию будет реализовано в полной версии.") + + def delete_employee(self): + employee_id = self.get_selected_employee_id() + if not employee_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить этого сотрудника?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("UPDATE employees SET is_active = 0 WHERE employee_id = ?", (employee_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Сотрудник удален.") + self.load_employees() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить сотрудника: {e}") + finally: + conn.close() + + # === Методы для материалов (заглушки) === + def add_material(self): + QMessageBox.information(self, "Информация", "Добавление материала будет реализовано в полной версии.") + + def edit_material(self): + QMessageBox.information(self, "Информация", "Редактирование материала будет реализовано в полной версии.") + + def view_stock_history(self): + QMessageBox.information(self, "Информация", "Просмотр истории запасов будет реализовано в полной версии.") + + def delete_material(self): + material_id = self.get_selected_material_id() + if not material_id: + return + + reply = QMessageBox.question( + self, "Подтверждение удаления", + "Вы уверены, что хотите удалить этот материал?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + conn = sqlite3.connect('masterpol.db') + cursor = conn.cursor() + try: + cursor.execute("DELETE FROM materials WHERE material_id = ?", (material_id,)) + conn.commit() + QMessageBox.information(self, "Успех", "Материал удален.") + self.load_materials() + except sqlite3.Error as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось удалить материал: {e}") + finally: + conn.close() + + +# === Точка входа === +if __name__ == "__main__": + # Инициализация базы данных + try: + init_database() + print("✅ База данных успешно инициализирована") + except Exception as e: + print(f"❌ Ошибка инициализации БД: {e}") + sys.exit(1) + + # Создание приложения + app = QApplication(sys.argv) + app.setFont(QFont(APP_STYLES['font_family'], 10)) + + # Авторизация + auth_dialog = AuthDialog() + if auth_dialog.exec() != QDialog.DialogCode.Accepted: + sys.exit(0) + + # Создание главного окна + main_window = MainWindow(auth_dialog.user_id, auth_dialog.user_name, auth_dialog.user_role) + main_window.show() + + sys.exit(app.exec()) diff --git a/masterpol.db b/masterpol.db index 5cb6c5f..6ff4e21 100644 Binary files a/masterpol.db and b/masterpol.db differ diff --git a/ressult/.env b/ressult/.env new file mode 100644 index 0000000..5a2d5c7 --- /dev/null +++ b/ressult/.env @@ -0,0 +1,6 @@ +# .env +DATABASE_URL=postgresql://postgres:213k2010###@localhost/masterpol +SECRET_KEY=your-secret-key-here +DEBUG=True +HOST=0.0.0.0 +PORT=8000 diff --git a/ressult/app/__pycache__/database.cpython-314.pyc b/ressult/app/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000..a03691d Binary files /dev/null and b/ressult/app/__pycache__/database.cpython-314.pyc differ diff --git a/ressult/app/__pycache__/main.cpython-314.pyc b/ressult/app/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000..b1c1369 Binary files /dev/null and b/ressult/app/__pycache__/main.cpython-314.pyc differ diff --git a/ressult/app/database.py b/ressult/app/database.py new file mode 100644 index 0000000..8006102 --- /dev/null +++ b/ressult/app/database.py @@ -0,0 +1,60 @@ +# app/database.py +""" +Модуль для работы с базой данных PostgreSQL +Соответствует требованиям ТЗ по разработке базы данных +""" +import os +import psycopg2 +from psycopg2.extras import RealDictCursor +from dotenv import load_dotenv +import time + +load_dotenv() + +class Database: + def __init__(self): + self.connection = None + self.max_retries = 3 + self.retry_delay = 1 + + def get_connection(self): + """Получение подключения к базе данных с повторными попытками""" + if self.connection is None or self.connection.closed: + for attempt in range(self.max_retries): + try: + self.connection = psycopg2.connect( + os.getenv('DATABASE_URL'), + cursor_factory=RealDictCursor + ) + break + except psycopg2.OperationalError as e: + if attempt < self.max_retries - 1: + time.sleep(self.retry_delay) + continue + else: + raise e + return self.connection + + def execute_query(self, query, params=None): + """Выполнение SQL запроса с обработкой ошибок""" + conn = self.get_connection() + try: + with conn.cursor() as cursor: + cursor.execute(query, params) + if query.strip().upper().startswith('SELECT'): + return cursor.fetchall() + conn.commit() + return cursor.rowcount + except psycopg2.InterfaceError: + self.connection = None + raise + except Exception as e: + conn.rollback() + raise e + + def close(self): + """Закрытие соединения с базой данных""" + if self.connection and not self.connection.closed: + self.connection.close() + +db = Database() diff --git a/ressult/app/main.py b/ressult/app/main.py new file mode 100644 index 0000000..65beb14 --- /dev/null +++ b/ressult/app/main.py @@ -0,0 +1,48 @@ +# app/main.py +""" +Главный модуль FastAPI приложения +Соответствует требованиям ТЗ по интеграции модулей +""" +import os +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from dotenv import load_dotenv +from app.routes import partners, sales, upload, calculations, auth, config + +load_dotenv() + +app = FastAPI( + title="MasterPol Partner Management System", + description="REST API для системы управления партнерами согласно ТЗ демонстрационного экзамена", + version="1.0.0" +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Регистрация маршрутов согласно модулям ТЗ +app.include_router(partners.router, prefix="/api/v1/partners", tags=["Partners Management"]) +app.include_router(sales.router, prefix="/api/v1/sales", tags=["Sales History"]) +app.include_router(upload.router, prefix="/api/v1/upload", tags=["Data Import"]) +app.include_router(calculations.router, prefix="/api/v1/calculations", tags=["Calculations"]) +app.include_router(config.router, prefix="/api/v1/config", tags=["Configuration"]) +app.include_router(auth.router, prefix="/api/v1/auth", tags=["Authentication"]) + +@app.get("/") +async def root(): + """Корневой endpoint системы""" + return { + "message": "MasterPol Partner Management System API", + "version": "1.0.0", + "description": "Система управления партнерами согласно ТЗ демонстрационного экзамена" + } + +@app.get("/health") +async def health_check(): + """Проверка здоровья приложения""" + return {"status": "healthy"} diff --git a/ressult/app/models/__init__.py b/ressult/app/models/__init__.py new file mode 100644 index 0000000..10fccba --- /dev/null +++ b/ressult/app/models/__init__.py @@ -0,0 +1,75 @@ +# app/models/__init__.py +""" +Модели данных Pydantic для валидации API запросов и ответов +Соответствует ТЗ демонстрационного экзамена +""" +from pydantic import BaseModel, EmailStr, validator, conint +from typing import Optional +from decimal import Decimal + +class PartnerBase(BaseModel): + partner_type: Optional[str] = None + company_name: str + legal_address: Optional[str] = None + inn: str + director_name: Optional[str] = None + phone: Optional[str] = None + email: Optional[EmailStr] = None + rating: conint(ge=0) # Рейтинг должен быть целым неотрицательным числом + sales_locations: Optional[str] = None + + @validator('phone') + def validate_phone(cls, v): + if v and not v.startswith('+'): + raise ValueError('Телефон должен начинаться с +') + return v + +class PartnerCreate(PartnerBase): + pass + +class PartnerUpdate(PartnerBase): + pass + +class Partner(PartnerBase): + partner_id: int + + class Config: + from_attributes = True + +class SaleBase(BaseModel): + partner_id: int + product_name: str + quantity: Decimal + sale_date: str + +class SaleCreate(SaleBase): + pass + +class Sale(SaleBase): + sale_id: int + + class Config: + from_attributes = True + +class UploadResponse(BaseModel): + message: str + processed_rows: int + errors: list[str] = [] + +class MaterialCalculationRequest(BaseModel): + product_type_id: int + material_type_id: int + quantity: conint(ge=1) + param1: float + param2: float + product_coeff: float + defect_percent: float + +class MaterialCalculationResponse(BaseModel): + material_quantity: int + status: str + +class DiscountResponse(BaseModel): + partner_id: int + total_sales: Decimal + discount_percent: int diff --git a/ressult/app/models/__pycache__/__init__.cpython-314.pyc b/ressult/app/models/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..b4c160b Binary files /dev/null and b/ressult/app/models/__pycache__/__init__.cpython-314.pyc differ diff --git a/ressult/app/routes/__init__.py b/ressult/app/routes/__init__.py new file mode 100644 index 0000000..23f8410 --- /dev/null +++ b/ressult/app/routes/__init__.py @@ -0,0 +1,5 @@ +# app/routes/__init__.py +""" +Инициализация маршрутов API +""" +from . import partners, sales, upload, calculations, auth, config diff --git a/ressult/app/routes/__pycache__/__init__.cpython-314.pyc b/ressult/app/routes/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..0ab0a5d Binary files /dev/null and b/ressult/app/routes/__pycache__/__init__.cpython-314.pyc differ diff --git a/ressult/app/routes/__pycache__/auth.cpython-314.pyc b/ressult/app/routes/__pycache__/auth.cpython-314.pyc new file mode 100644 index 0000000..76a68b9 Binary files /dev/null and b/ressult/app/routes/__pycache__/auth.cpython-314.pyc differ diff --git a/ressult/app/routes/__pycache__/calculations.cpython-314.pyc b/ressult/app/routes/__pycache__/calculations.cpython-314.pyc new file mode 100644 index 0000000..cf8e18a Binary files /dev/null and b/ressult/app/routes/__pycache__/calculations.cpython-314.pyc differ diff --git a/ressult/app/routes/__pycache__/config.cpython-314.pyc b/ressult/app/routes/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000..f2a5e23 Binary files /dev/null and b/ressult/app/routes/__pycache__/config.cpython-314.pyc differ diff --git a/ressult/app/routes/__pycache__/partners.cpython-314.pyc b/ressult/app/routes/__pycache__/partners.cpython-314.pyc new file mode 100644 index 0000000..0a432e2 Binary files /dev/null and b/ressult/app/routes/__pycache__/partners.cpython-314.pyc differ diff --git a/ressult/app/routes/__pycache__/sales.cpython-314.pyc b/ressult/app/routes/__pycache__/sales.cpython-314.pyc new file mode 100644 index 0000000..77df468 Binary files /dev/null and b/ressult/app/routes/__pycache__/sales.cpython-314.pyc differ diff --git a/ressult/app/routes/__pycache__/upload.cpython-314.pyc b/ressult/app/routes/__pycache__/upload.cpython-314.pyc new file mode 100644 index 0000000..a09ba33 Binary files /dev/null and b/ressult/app/routes/__pycache__/upload.cpython-314.pyc differ diff --git a/ressult/app/routes/auth.py b/ressult/app/routes/auth.py new file mode 100644 index 0000000..acae8ac --- /dev/null +++ b/ressult/app/routes/auth.py @@ -0,0 +1,45 @@ +# app/routes/auth.py +""" +Маршруты API для аутентификации +""" +from fastapi import APIRouter, HTTPException, Depends +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from app.database import db +import bcrypt + +router = APIRouter() +security = HTTPBasic() + +@router.post("/login") +async def login(credentials: HTTPBasicCredentials = Depends(security)): + """Аутентификация менеджера""" + try: + result = db.execute_query( + "SELECT manager_id, username, password_hash, full_name FROM managers WHERE username = %s AND is_active = TRUE", + (credentials.username,) + ) + + if not result: + raise HTTPException(status_code=401, detail="Invalid credentials") + + manager = dict(result[0]) + stored_hash = manager['password_hash'] + + # Проверка пароля + if bcrypt.checkpw(credentials.password.encode('utf-8'), stored_hash.encode('utf-8')): + return { + "manager_id": manager['manager_id'], + "username": manager['username'], + "full_name": manager['full_name'], + "authenticated": True + } + else: + raise HTTPException(status_code=401, detail="Invalid credentials") + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/verify") +async def verify_token(): + """Проверка валидности токена""" + return {"verified": True} diff --git a/ressult/app/routes/calculations.py b/ressult/app/routes/calculations.py new file mode 100644 index 0000000..c41200b --- /dev/null +++ b/ressult/app/routes/calculations.py @@ -0,0 +1,43 @@ +# app/routes/calculations.py +""" +Маршруты API для расчетов +Соответствует модулю 4 ТЗ по расчету материалов +""" +from fastapi import APIRouter, HTTPException +from app.models import MaterialCalculationRequest, MaterialCalculationResponse +import math + +router = APIRouter() + +@router.post("/calculate-material", response_model=MaterialCalculationResponse) +async def calculate_material(request: MaterialCalculationRequest): + """ + Расчет количества материала для производства продукции + Соответствует модулю 4 ТЗ + """ + try: + # Валидация входных параметров + if (request.param1 <= 0 or request.param2 <= 0 or + request.product_coeff <= 0 or request.defect_percent < 0): + return MaterialCalculationResponse( + material_quantity=-1, + status="error: invalid parameters" + ) + + # Расчет количества материала на одну единицу продукции + material_per_unit = request.param1 * request.param2 * request.product_coeff + + # Расчет общего количества материала с учетом брака + total_material = material_per_unit * request.quantity + total_material_with_defect = total_material * (1 + request.defect_percent / 100) + + # Округление до целого числа в большую сторону + material_quantity = math.ceil(total_material_with_defect) + + return MaterialCalculationResponse( + material_quantity=material_quantity, + status="success" + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ressult/app/routes/config.py b/ressult/app/routes/config.py new file mode 100644 index 0000000..10e68ea --- /dev/null +++ b/ressult/app/routes/config.py @@ -0,0 +1,32 @@ +# app/routes/config.py +""" +Маршруты API для управления конфигурацией +""" +from fastapi import APIRouter, HTTPException +from pathlib import Path +import json + +router = APIRouter() + +CONFIG_PATH = Path(__file__).parent.parent.parent / "config.json" + +@router.get("/") +async def get_config(): + """Получение текущей конфигурации""" + try: + if CONFIG_PATH.exists(): + with open(CONFIG_PATH, 'r', encoding='utf-8') as f: + return json.load(f) + return {"message": "Config file not found"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error reading config: {str(e)}") + +@router.put("/") +async def update_config(config_data: dict): + """Обновление конфигурации""" + try: + with open(CONFIG_PATH, 'w', encoding='utf-8') as f: + json.dump(config_data, f, indent=4, ensure_ascii=False) + return {"message": "Configuration updated successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error saving config: {str(e)}") diff --git a/ressult/app/routes/partners.py b/ressult/app/routes/partners.py new file mode 100644 index 0000000..8c64889 --- /dev/null +++ b/ressult/app/routes/partners.py @@ -0,0 +1,157 @@ +# app/routes/partners.py +""" +Маршруты API для управления партнерами +Соответствует модулям 1-3 ТЗ +""" +from fastapi import APIRouter, HTTPException +from app.database import db +from app.models import Partner, PartnerCreate, PartnerUpdate, DiscountResponse +from decimal import Decimal + +router = APIRouter() + +@router.get("/") +async def get_partners(): + """ + Получение списка всех партнеров + Соответствует требованию просмотра списка партнеров + """ + try: + result = db.execute_query(""" + SELECT partner_id, partner_type, company_name, legal_address, + inn, director_name, phone, email, rating, sales_locations + FROM partners + ORDER BY company_name + """) + + partners_list = [] + for row in result: + partner_dict = dict(row) + # Преобразуем рейтинг к int если нужно + if isinstance(partner_dict.get('rating'), float): + partner_dict['rating'] = int(partner_dict['rating']) + partners_list.append(partner_dict) + + return partners_list + + except Exception as e: + if "relation \"partners\" does not exist" in str(e): + return [] + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/{partner_id}") +async def get_partner(partner_id: int): + """Получение информации о конкретном партнере""" + try: + result = db.execute_query( + "SELECT * FROM partners WHERE partner_id = %s", + (partner_id,) + ) + if not result: + raise HTTPException(status_code=404, detail="Partner not found") + + partner_data = dict(result[0]) + # Преобразуем рейтинг к int если нужно + if isinstance(partner_data.get('rating'), float): + partner_data['rating'] = int(partner_data['rating']) + + return partner_data + + except Exception as e: + if "relation \"partners\" does not exist" in str(e): + raise HTTPException(status_code=404, detail="Partner not found") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/") +async def create_partner(partner: PartnerCreate): + """ + Создание нового партнера + Включает валидацию данных согласно ТЗ + """ + try: + result = db.execute_query(""" + INSERT INTO partners + (partner_type, company_name, legal_address, inn, director_name, + phone, email, rating, sales_locations) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING partner_id + """, ( + partner.partner_type, partner.company_name, partner.legal_address, + partner.inn, partner.director_name, partner.phone, partner.email, + partner.rating, partner.sales_locations + )) + return {"partner_id": result[0]["partner_id"]} + except Exception as e: + if "duplicate key value violates unique constraint" in str(e): + raise HTTPException(status_code=400, detail="Partner with this INN already exists") + raise HTTPException(status_code=500, detail=str(e)) + +@router.put("/{partner_id}") +async def update_partner(partner_id: int, partner: PartnerUpdate): + """ + Обновление данных партнера + Соответствует требованию редактирования данных партнера + """ + try: + db.execute_query(""" + UPDATE partners SET + partner_type = %s, company_name = %s, legal_address = %s, + inn = %s, director_name = %s, phone = %s, email = %s, + rating = %s, sales_locations = %s + WHERE partner_id = %s + """, ( + partner.partner_type, partner.company_name, partner.legal_address, + partner.inn, partner.director_name, partner.phone, partner.email, + partner.rating, partner.sales_locations, partner_id + )) + return {"message": "Partner updated successfully"} + except Exception as e: + if "duplicate key value violates unique constraint" in str(e): + raise HTTPException(status_code=400, detail="Partner with this INN already exists") + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete("/{partner_id}") +async def delete_partner(partner_id: int): + """Удаление партнера""" + try: + db.execute_query( + "DELETE FROM partners WHERE partner_id = %s", + (partner_id,) + ) + return {"message": "Partner deleted successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/{partner_id}/discount", response_model=DiscountResponse) +async def calculate_partner_discount(partner_id: int): + """ + Расчет скидки для партнера на основе общего количества продаж + Соответствует модулю 2 ТЗ + """ + try: + # Получаем общее количество продаж партнера + result = db.execute_query(""" + SELECT COALESCE(SUM(quantity), 0) as total_sales + FROM sales WHERE partner_id = %s + """, (partner_id,)) + + total_sales = result[0]["total_sales"] if result else Decimal('0') + + # Расчет скидки согласно бизнес-правилам ТЗ + if total_sales < 10000: + discount = 0 + elif total_sales < 50000: + discount = 5 + elif total_sales < 300000: + discount = 10 + else: + discount = 15 + + return DiscountResponse( + partner_id=partner_id, + total_sales=total_sales, + discount_percent=discount + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ressult/app/routes/sales.py b/ressult/app/routes/sales.py new file mode 100644 index 0000000..fac35e0 --- /dev/null +++ b/ressult/app/routes/sales.py @@ -0,0 +1,64 @@ +# app/routes/sales.py +""" +Маршруты API для управления продажами +Соответствует требованиям ТЗ по истории реализации продукции +""" +from fastapi import APIRouter, HTTPException +from app.database import db +from app.models import Sale, SaleCreate + +router = APIRouter() + +@router.get("/partner/{partner_id}") +async def get_sales_by_partner(partner_id: int): + """ + Получение истории реализации продукции партнером + Соответствует модулю 4 ТЗ + """ + try: + result = db.execute_query(""" + SELECT sale_id, partner_id, product_name, quantity, sale_date + FROM sales + WHERE partner_id = %s + ORDER BY sale_date DESC + """, (partner_id,)) + return [dict(row) for row in result] + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/") +async def get_all_sales(): + """Получение всех продаж с информацией о партнерах""" + try: + result = db.execute_query(""" + SELECT s.sale_id, s.partner_id, p.company_name, s.product_name, + s.quantity, s.sale_date + FROM sales s + JOIN partners p ON s.partner_id = p.partner_id + ORDER BY s.sale_date DESC + """) + return [dict(row) for row in result] + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/") +async def create_sale(sale: SaleCreate): + """Создание новой записи о продаже""" + try: + result = db.execute_query(""" + INSERT INTO sales (partner_id, product_name, quantity, sale_date) + VALUES (%s, %s, %s, %s) + RETURNING sale_id + """, (sale.partner_id, sale.product_name, sale.quantity, sale.sale_date)) + return {"sale_id": result[0]["sale_id"]} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete("/{sale_id}") +async def delete_sale(sale_id: int): + """Удаление записи о продаже""" + try: + db.execute_query("DELETE FROM sales WHERE sale_id = %s", (sale_id,)) + return {"message": "Sale deleted successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ressult/app/routes/upload.py b/ressult/app/routes/upload.py new file mode 100644 index 0000000..5e4339d --- /dev/null +++ b/ressult/app/routes/upload.py @@ -0,0 +1,103 @@ +# app/routes/upload.py +""" +Маршруты API для загрузки и импорта данных +Соответствует требованиям ТЗ по импорту данных +""" +import pandas as pd +from fastapi import APIRouter, UploadFile, File, HTTPException +from app.database import db +from app.models import UploadResponse + +router = APIRouter() + +@router.post("/partners") +async def upload_partners(file: UploadFile = File(...)): + """ + Загрузка партнеров из файла + Подготовка данных для импорта согласно ТЗ + """ + try: + if file.filename.endswith('.xlsx'): + df = pd.read_excel(file.file) + elif file.filename.endswith('.csv'): + df = pd.read_csv(file.file) + else: + raise HTTPException(status_code=400, detail="Unsupported file format") + + processed = 0 + errors = [] + + for index, row in df.iterrows(): + try: + # Валидация и преобразование данных + rating = row.get('rating', 0) + if pd.isna(rating): + rating = 0 + + db.execute_query(""" + INSERT INTO partners + (partner_type, company_name, legal_address, inn, director_name, + phone, email, rating, sales_locations) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + row.get('partner_type'), + row.get('company_name'), + row.get('legal_address'), + row.get('inn'), + row.get('director_name'), + row.get('phone'), + row.get('email'), + int(rating), # Конвертация в целое число + row.get('sales_locations') + )) + processed += 1 + except Exception as e: + errors.append(f"Row {index}: {str(e)}") + + return UploadResponse( + message="File processed successfully", + processed_rows=processed, + errors=errors + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/sales") +async def upload_sales(file: UploadFile = File(...)): + """Загрузка продаж из файла""" + try: + if file.filename.endswith('.xlsx'): + df = pd.read_excel(file.file) + elif file.filename.endswith('.csv'): + df = pd.read_csv(file.file) + else: + raise HTTPException(status_code=400, detail="Unsupported file format") + + processed = 0 + errors = [] + + for index, row in df.iterrows(): + try: + db.execute_query(""" + INSERT INTO sales + (partner_id, product_name, quantity, sale_date) + VALUES (%s, %s, %s, %s) + """, ( + int(row.get('partner_id')), + row.get('product_name'), + row.get('quantity'), + row.get('sale_date') + )) + processed += 1 + except Exception as e: + errors.append(f"Row {index}: {str(e)}") + + return UploadResponse( + message="File processed successfully", + processed_rows=processed, + errors=errors + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ressult/config.json b/ressult/config.json new file mode 100644 index 0000000..a73381d --- /dev/null +++ b/ressult/config.json @@ -0,0 +1,24 @@ +{ + "application": { + "name": "MasterPol Partner Management System", + "version": "1.0.0", + "company_logo": "resources/logo.png", + "app_icon": "resources/icon.png" + }, + "api": { + "base_url": "http://localhost:8000", + "timeout": 30 + }, + "style": { + "primary_color": "#007acc", + "secondary_color": "#005a9e", + "accent_color": "#28a745", + "font_family": "Arial", + "font_size": "12px" + }, + "features": { + "enable_import": true, + "enable_export": true, + "enable_calculations": true + } +} diff --git a/ressult/database_init.py b/ressult/database_init.py new file mode 100644 index 0000000..a0ac362 --- /dev/null +++ b/ressult/database_init.py @@ -0,0 +1,196 @@ +# database_init.py +""" +Скрипт инициализации базы данных с исправлением ошибки типа данных +""" +import argparse +import sys +import os +from app.database import db +import bcrypt + +def parse_arguments(): + """Парсинг аргументов командной строки""" + parser = argparse.ArgumentParser(description='Инициализация базы данных MasterPol') + parser.add_argument('--host', default='localhost', help='Хост PostgreSQL') + parser.add_argument('--port', default='5432', help='Порт PostgreSQL') + parser.add_argument('--database', default='masterpol', help='Имя базы данных') + parser.add_argument('--username', default='postgres', help='Имя пользователя PostgreSQL') + parser.add_argument('--password', required=True, help='Пароль пользователя PostgreSQL') + + return parser.parse_args() + +def initialize_database(db_url): + """Инициализация структуры базы данных с тестовыми данными""" + + # Устанавливаем URL базы данных + os.environ['DATABASE_URL'] = db_url + + # Удаляем существующие таблицы (для чистой инициализации) + drop_tables = """ + DROP TABLE IF EXISTS sales CASCADE; + DROP TABLE IF EXISTS partners CASCADE; + DROP TABLE IF EXISTS managers CASCADE; + """ + + # Создание таблицы партнеров с правильным типом для rating + partners_table = """ + CREATE TABLE IF NOT EXISTS partners ( + partner_id SERIAL PRIMARY KEY, + partner_type VARCHAR(50), + company_name VARCHAR(255) NOT NULL, + legal_address TEXT, + inn VARCHAR(20) UNIQUE NOT NULL, + director_name VARCHAR(255), + phone VARCHAR(50), + email VARCHAR(255), + rating INTEGER NOT NULL DEFAULT 0 CHECK (rating >= 0 AND rating <= 100), + sales_locations TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + + # Создание таблицы продаж + sales_table = """ + CREATE TABLE IF NOT EXISTS sales ( + sale_id SERIAL PRIMARY KEY, + partner_id INTEGER NOT NULL REFERENCES partners(partner_id) ON DELETE CASCADE, + product_name VARCHAR(255) NOT NULL, + quantity DECIMAL(15,2) NOT NULL, + sale_date DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + + # Создание таблицы менеджеров + managers_table = """ + CREATE TABLE IF NOT EXISTS managers ( + manager_id SERIAL PRIMARY KEY, + username VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + full_name VARCHAR(255) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + + try: + # Удаляем существующие таблицы + try: + db.execute_query(drop_tables) + print("✅ Существующие таблицы удалены") + except Exception as e: + print(f"ℹ️ Таблицы для удаления не найдены: {e}") + + # Создание таблиц + db.execute_query(partners_table) + db.execute_query(sales_table) + db.execute_query(managers_table) + print("✅ База данных успешно инициализирована") + + # Создание тестового менеджера + password = "pass123" + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + + db.execute_query(""" + INSERT INTO managers (username, password_hash, full_name) + VALUES ('manager', %s, 'Тестовый Менеджер') + ON CONFLICT (username) DO NOTHING + """, (hashed_password,)) + print("✅ Тестовый пользователь создан (manager/pass123)") + + # Добавление тестовых партнеров + test_partners = [ + { + 'partner_type': 'distributor', + 'company_name': 'ООО "Ромашка"', + 'legal_address': 'г. Москва, ул. Ленина, д. 1', + 'inn': '1234567890', + 'director_name': 'Иванов Иван Иванович', + 'phone': '+79991234567', + 'email': 'info@romashka.ru', + 'rating': 85, # INTEGER значение от 0 до 100 + 'sales_locations': 'Москва, Санкт-Петербург' + }, + { + 'partner_type': 'retail', + 'company_name': 'ИП Петров', + 'legal_address': 'г. Санкт-Петербург, Невский пр., д. 100', + 'inn': '0987654321', + 'director_name': 'Петров Петр Петрович', + 'phone': '+79998765432', + 'email': 'petrov@mail.ru', + 'rating': 72, # INTEGER значение от 0 до 100 + 'sales_locations': 'Санкт-Петербург' + } + ] + + for partner in test_partners: + db.execute_query(""" + INSERT INTO partners + (partner_type, company_name, legal_address, inn, director_name, + phone, email, rating, sales_locations) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + partner['partner_type'], partner['company_name'], + partner['legal_address'], partner['inn'], + partner['director_name'], partner['phone'], + partner['email'], partner['rating'], + partner['sales_locations'] + )) + + print("✅ Тестовые партнеры добавлены") + + # Добавление тестовых продаж + test_sales = [ + (1, 'Продукт А', 150.50, '2024-01-15'), + (1, 'Продукт Б', 75.25, '2024-01-16'), + (2, 'Продукт В', 200.00, '2024-01-17'), + (1, 'Продукт А', 100.00, '2024-01-18') + ] + + for sale in test_sales: + db.execute_query(""" + INSERT INTO sales (partner_id, product_name, quantity, sale_date) + VALUES (%s, %s, %s, %s) + """, sale) + + print("✅ Тестовые продажи добавлены") + + # Проверяем, что данные корректно добавлены + partners_count = db.execute_query("SELECT COUNT(*) as count FROM partners")[0]['count'] + sales_count = db.execute_query("SELECT COUNT(*) as count FROM sales")[0]['count'] + managers_count = db.execute_query("SELECT COUNT(*) as count FROM managers")[0]['count'] + + print(f"📊 Статистика базы данных:") + print(f" - Партнеров: {partners_count}") + print(f" - Продаж: {sales_count}") + print(f" - Менеджеров: {managers_count}") + + return True + + except Exception as e: + print(f"❌ Ошибка инициализации базы данных: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """Основная функция""" + args = parse_arguments() + + # Формируем URL подключения + db_url = f"postgresql://{args.username}:{args.password}@{args.host}:{args.port}/{args.database}" + + print(f"🔄 Подключение к базе данных: {args.database} на {args.host}:{args.port}") + + success = initialize_database(db_url) + + if success: + print("🎉 Инициализация базы данных завершена успешно!") + sys.exit(0) + else: + print("💥 Инициализация базы данных завершена с ошибками!") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/ressult/gui/__init__.py b/ressult/gui/__init__.py new file mode 100644 index 0000000..7496f80 --- /dev/null +++ b/ressult/gui/__init__.py @@ -0,0 +1,9 @@ +# gui/__init__.py +""" +Пакет графического интерфейса с авторизацией +""" +from .login_window import LoginWindow +from .main_window import MainWindow +from .partner_form import PartnerForm +from .sales_history import SalesHistoryWindow +from .material_calculator import MaterialCalculatorWindow diff --git a/ressult/gui/__pycache__/__init__.cpython-314.pyc b/ressult/gui/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..844ae5c Binary files /dev/null and b/ressult/gui/__pycache__/__init__.cpython-314.pyc differ diff --git a/ressult/gui/__pycache__/login_window.cpython-314.pyc b/ressult/gui/__pycache__/login_window.cpython-314.pyc new file mode 100644 index 0000000..30f372b Binary files /dev/null and b/ressult/gui/__pycache__/login_window.cpython-314.pyc differ diff --git a/ressult/gui/__pycache__/main_window.cpython-314.pyc b/ressult/gui/__pycache__/main_window.cpython-314.pyc new file mode 100644 index 0000000..bd6114a Binary files /dev/null and b/ressult/gui/__pycache__/main_window.cpython-314.pyc differ diff --git a/ressult/gui/__pycache__/material_calculator.cpython-314.pyc b/ressult/gui/__pycache__/material_calculator.cpython-314.pyc new file mode 100644 index 0000000..b5a2627 Binary files /dev/null and b/ressult/gui/__pycache__/material_calculator.cpython-314.pyc differ diff --git a/ressult/gui/__pycache__/partner_form.cpython-314.pyc b/ressult/gui/__pycache__/partner_form.cpython-314.pyc new file mode 100644 index 0000000..63a8845 Binary files /dev/null and b/ressult/gui/__pycache__/partner_form.cpython-314.pyc differ diff --git a/ressult/gui/__pycache__/sales_history.cpython-314.pyc b/ressult/gui/__pycache__/sales_history.cpython-314.pyc new file mode 100644 index 0000000..8bb9689 Binary files /dev/null and b/ressult/gui/__pycache__/sales_history.cpython-314.pyc differ diff --git a/ressult/gui/login_window.py b/ressult/gui/login_window.py new file mode 100644 index 0000000..c61b70b --- /dev/null +++ b/ressult/gui/login_window.py @@ -0,0 +1,253 @@ +# gui/login_window.py +""" +Окно авторизации менеджера +Соответствует требованиям ТЗ по аутентификации +""" +import sys +from PyQt6.QtWidgets import (QApplication, QDialog, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QMessageBox, + QFrame, QCheckBox) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont, QPixmap, QIcon +import requests +from requests.auth import HTTPBasicAuth + +class LoginWindow(QDialog): + """Окно авторизации системы MasterPol""" + + login_success = pyqtSignal(dict) # Сигнал об успешной авторизации + + def __init__(self): + super().__init__() + self.setup_ui() + self.load_settings() + + def setup_ui(self): + """Настройка интерфейса окна авторизации""" + self.setWindowTitle("MasterPol - Авторизация") + self.setFixedSize(400, 500) + self.setModal(True) + + # Установка иконки приложения + try: + self.setWindowIcon(QIcon("resources/icon.png")) + except: + pass + + layout = QVBoxLayout() + layout.setContentsMargins(30, 30, 30, 30) + layout.setSpacing(0) + + # Заголовок + title_label = QLabel("MasterPol") + title_label.setFont(QFont("Arial", 24, QFont.Weight.Bold)) + title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + title_label.setStyleSheet("color: #007acc; margin-bottom: 20px;") + + subtitle_label = QLabel("Система управления партнерами") + subtitle_label.setFont(QFont("Arial", 12)) + subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + subtitle_label.setStyleSheet("color: #666; margin-bottom: 30px;") + + layout.addWidget(title_label) + layout.addWidget(subtitle_label) + + # Форма авторизаци + form_frame = QFrame() + form_frame.setStyleSheet(""" + QFrame { + background-color: white; + border: 0px solid #ddd; + border-radius: 4px; + padding: 20px; + } + """) + + form_layout = QVBoxLayout() + form_layout.setSpacing(15) + + # Поле логина + username_layout = QVBoxLayout() + username_label = QLabel("Имя пользователя:") + username_label.setStyleSheet("font-weight: bold; color: #333;") + + self.username_input = QLineEdit() + self.username_input.setPlaceholderText("Введите имя пользователя") + self.username_input.setStyleSheet(""" + QLineEdit { + padding: 8px 12px; + border: 2px solid #ccc; + border-radius: 6px; + font-size: 14px; + } + QLineEdit:focus { + border-color: #007acc; + } + """) + + username_layout.addWidget(username_label) + username_layout.addWidget(self.username_input) + + # Поле пароля + password_layout = QVBoxLayout() + password_label = QLabel("Пароль:") + password_label.setStyleSheet("font-weight: bold; color: #333;") + + self.password_input = QLineEdit() + self.password_input.setPlaceholderText("Введите пароль") + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) + self.password_input.setStyleSheet(""" + QLineEdit { + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + } + QLineEdit:focus { + border-color: #007acc; + } + """) + + password_layout.addWidget(password_label) + password_layout.addWidget(self.password_input) + + # Запомнить меня + self.remember_checkbox = QCheckBox("Запомнить меня") + self.remember_checkbox.setStyleSheet("color: #333;") + + # Кнопка входа + self.login_button = QPushButton("Войти в систему") + self.login_button.clicked.connect(self.authenticate) + self.login_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 12px; + border-radius: 4px; + font-weight: bold; + font-size: 14px; + } + QPushButton:hover { + background-color: #005a9e; + } + QPushButton:disabled { + background-color: #ccc; + color: #666; + } + """) + + # Подсказка + hint_label = QLabel("Используйте логин: manager, пароль: pass123") + hint_label.setStyleSheet("color: #666; font-size: 12px; margin-top: 10px;") + hint_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + form_layout.addLayout(username_layout) + form_layout.addLayout(password_layout) + form_layout.addWidget(self.remember_checkbox) + form_layout.addWidget(self.login_button) + form_layout.addWidget(hint_label) + + form_frame.setLayout(form_layout) + layout.addWidget(form_frame) + + # Информация о системе + info_label = QLabel("MasterPol v1.0.0\nСистема управления партнерами и продажами") + info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + info_label.setStyleSheet("color: #999; font-size: 11px; margin-top: 20px;") + layout.addWidget(info_label) + + self.setLayout(layout) + + # Подключаем обработчики событий + self.username_input.returnPressed.connect(self.authenticate) + self.password_input.returnPressed.connect(self.authenticate) + + def load_settings(self): + """Загрузка сохраненных настроек авторизации""" + try: + # Здесь можно добавить загрузку из файла настроек + # Пока просто устанавливаем значения по умолчанию + self.username_input.setText("manager") + except: + pass + + def save_settings(self): + """Сохранение настроек авторизации""" + if self.remember_checkbox.isChecked(): + # Здесь можно добавить сохранение в файл настроек + pass + + def authenticate(self): + """Аутентификация пользователя""" + username = self.username_input.text().strip() + password = self.password_input.text().strip() + + if not username or not password: + QMessageBox.warning(self, "Ошибка", "Заполните все поля") + return + + # Блокируем кнопку во время аутентификации + self.login_button.setEnabled(False) + self.login_button.setText("Проверка...") + + try: + # Выполняем аутентификацию через API + response = requests.post( + "http://localhost:8000/api/v1/auth/login", + auth=HTTPBasicAuth(username, password), + timeout=10 + ) + + if response.status_code == 200: + user_data = response.json() + + # Сохраняем настройки + self.save_settings() + + # Сохраняем учетные данные для будущих запросов + user_data['auth'] = HTTPBasicAuth(username, password) + + # Отправляем сигнал об успешной авторизации + self.login_success.emit(user_data) + + else: + QMessageBox.warning( + self, + "Ошибка авторизации", + "Неверное имя пользователя или пароль" + ) + + except requests.exceptions.ConnectionError: + QMessageBox.critical( + self, + "Ошибка подключения", + "Не удалось подключиться к серверу.\n" + "Убедитесь, что сервер запущен на localhost:8000" + ) + except requests.exceptions.Timeout: + QMessageBox.critical( + self, + "Ошибка подключения", + "Превышено время ожидания ответа от сервера" + ) + except Exception as e: + QMessageBox.critical( + self, + "Ошибка", + f"Произошла непредвиденная ошибка:\n{str(e)}" + ) + finally: + # Разблокируем кнопку + self.login_button.setEnabled(True) + self.login_button.setText("Войти в систему") + +def main(): + """Точка входа для тестирования окна авторизации""" + app = QApplication(sys.argv) + window = LoginWindow() + window.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/ressult/gui/main_window b/ressult/gui/main_window new file mode 100644 index 0000000..8af89e8 --- /dev/null +++ b/ressult/gui/main_window @@ -0,0 +1,574 @@ +# gui/main_window.py +""" +Главное окно приложения PyQt6 с поддержкой авторизации +""" +import sys +import requests +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QLabel, QPushButton, QListWidget, + QListWidgetItem, QMessageBox, QFrame, QStackedWidget, + QMenuBar, QMenu, QStatusBar, QToolBar) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction +from .partner_form import PartnerForm +from .sales_history import SalesHistoryWindow +from .material_calculator import MaterialCalculatorWindow + +class PartnerCard(QFrame): + """Карточка партнера для отображения в списке""" + partner_clicked = pyqtSignal(dict) + + def __init__(self, partner_data): + super().__init__() + self.partner_data = partner_data + self.setup_ui() + + def setup_ui(self): + self.setFrameStyle(QFrame.Shape.StyledPanel) + self.setStyleSheet(""" + PartnerCard { + background-color: white; + border: 1px solid #ddd; + border-radius: 8px; + padding: 12px; + margin: 4px; + } + PartnerCard:hover { + background-color: #f5f5f5; + border-color: #007acc; + } + """) + + layout = QVBoxLayout() + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(4) + + # Заголовок с типом и названием + header_layout = QHBoxLayout() + header_layout.setSpacing(4) + + type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |") + type_label.setStyleSheet("color: #666; font-weight: bold;") + + name_label = QLabel(self.partner_data['company_name']) + name_label.setStyleSheet("font-weight: bold; font-size: 14px;") + name_label.setWordWrap(True) + + # Безопасное преобразование рейтинга + rating_value = self.partner_data.get('rating', 0) + if isinstance(rating_value, float): + rating_value = int(rating_value) + + rating_label = QLabel(f"{rating_value}%") + rating_label.setStyleSheet("color: #007acc; font-weight: bold;") + + header_layout.addWidget(type_label) + header_layout.addWidget(name_label) + header_layout.addStretch() + header_layout.addWidget(rating_label) + + # Информация о директоре + director_label = QLabel(self.partner_data.get('director_name', 'Директор не указан')) + director_label.setStyleSheet("color: #444;") + + # Контактная информация + phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан')) + phone_label.setStyleSheet("color: #666;") + + layout.addLayout(header_layout) + layout.addWidget(director_label) + layout.addWidget(phone_label) + + self.setLayout(layout) + + def mousePressEvent(self, event): + """Обработка клика на карточке""" + if event.button() == Qt.MouseButton.LeftButton: + self.partner_clicked.emit(self.partner_data) + +class MainWindow(QMainWindow): + """Главное окно приложения с поддержкой авторизации""" + + def __init__(self, user_data): + super().__init__() + self.user_data = user_data + self.current_partner = None + self.auth = user_data.get('auth') + self.setup_ui() + self.load_partners() + + def setup_ui(self): + """Настройка интерфейса главного окна""" + self.setWindowTitle(f"MasterPol - Система управления партнерами") + self.setGeometry(100, 100, 1200, 700) + + # Установка иконки приложения + try: + self.setWindowIcon(QIcon("resources/icon.png")) + except: + pass + + # Создание меню + self.create_menu() + + # Создание тулбара + self.create_toolbar() + + # Создание статусной строки + self.create_statusbar() + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + main_layout = QHBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + + # Левая панель - список партнеров + left_panel = self.create_partners_panel() + main_layout.addWidget(left_panel, 1) + + # Правая панель - детальная информация + self.right_panel = self.create_details_panel() + main_layout.addWidget(self.right_panel, 2) + + central_widget.setLayout(main_layout) + + def create_menu(self): + """Создание меню приложения""" + menubar = self.menuBar() + + # Меню Файл + file_menu = menubar.addMenu('Файл') + + refresh_action = QAction('Обновить', self) + refresh_action.setShortcut('F5') + refresh_action.triggered.connect(self.load_partners) + file_menu.addAction(refresh_action) + + file_menu.addSeparator() + + logout_action = QAction('Выход', self) + logout_action.setShortcut('Ctrl+Q') + logout_action.triggered.connect(self.logout) + file_menu.addAction(logout_action) + + # Меню Сервис + service_menu = menubar.addMenu('Сервис') + + calc_action = QAction('Калькулятор материалов', self) + calc_action.triggered.connect(self.show_material_calculator) + service_menu.addAction(calc_action) + + # Меню Справка + help_menu = menubar.addMenu('Справка') + + about_action = QAction('О программе', self) + about_action.triggered.connect(self.show_about) + help_menu.addAction(about_action) + + def create_toolbar(self): + """Создание панели инструментов""" + toolbar = QToolBar("Основные инструменты") + self.addToolBar(toolbar) + + refresh_action = QAction('Обновить', self) + refresh_action.triggered.connect(self.load_partners) + toolbar.addAction(refresh_action) + + toolbar.addSeparator() + + add_partner_action = QAction('Добавить партнера', self) + add_partner_action.triggered.connect(self.show_add_partner_form) + toolbar.addAction(add_partner_action) + + def create_statusbar(self): + """Создание статусной строки""" + statusbar = self.statusBar() + user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}" + statusbar.showMessage(user_info) + + def create_partners_panel(self): + """Создание панели списка партнеров""" + panel = QWidget() + panel.setMaximumWidth(400) + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок + title = QLabel("Партнеры") + title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("padding: 10px;") + layout.addWidget(title) + + # Панель управления + control_layout = QHBoxLayout() + control_layout.setSpacing(10) + + self.add_button = QPushButton("Добавить партнера") + self.add_button.clicked.connect(self.show_add_partner_form) + self.add_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.refresh_button = QPushButton("Обновить") + self.refresh_button.clicked.connect(self.load_partners) + self.refresh_button.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #545b62; + } + """) + + control_layout.addWidget(self.add_button) + control_layout.addWidget(self.refresh_button) + control_layout.addStretch() + + layout.addLayout(control_layout) + + # Список партнеров + self.partners_list = QListWidget() + self.partners_list.setStyleSheet(""" + QListWidget { + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + outline: none; + } + QListWidget::item { + border: none; + padding: 0px; + } + QListWidget::item:selected { + background-color: transparent; + } + """) + layout.addWidget(self.partners_list) + + # Кнопка расчета материалов + self.calc_button = QPushButton("Калькулятор материалов") + self.calc_button.clicked.connect(self.show_material_calculator) + self.calc_button.setStyleSheet(""" + QPushButton { + background-color: #17a2b8; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #138496; + } + """) + layout.addWidget(self.calc_button) + + panel.setLayout(layout) + return panel + + def create_details_panel(self): + """Создание панели детальной информации""" + panel = QWidget() + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок детальной информации + self.details_title = QLabel("Выберите партнера") + self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold)) + self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.details_title.setStyleSheet("padding: 10px;") + layout.addWidget(self.details_title) + + # Детальная информация о партнере - создаем пустой frame + self.details_frame = QFrame() + self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + self.details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + self.details_layout = QVBoxLayout() + self.details_layout.setSpacing(8) + self.details_frame.setLayout(self.details_layout) + self.details_frame.hide() + + layout.addWidget(self.details_frame) + + # Кнопки управления выбранным партнером + self.control_buttons = QWidget() + buttons_layout = QHBoxLayout() + buttons_layout.setSpacing(10) + + self.edit_button = QPushButton("Редактировать") + self.edit_button.clicked.connect(self.edit_partner) + self.edit_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + self.edit_button.hide() + + self.sales_button = QPushButton("История продаж") + self.sales_button.clicked.connect(self.show_sales_history) + self.sales_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + self.sales_button.hide() + + self.discount_button = QPushButton("Расчет скидки") + self.discount_button.clicked.connect(self.calculate_discount) + self.discount_button.setStyleSheet(""" + QPushButton { + background-color: #ffc107; + color: black; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #e0a800; + } + """) + self.discount_button.hide() + + buttons_layout.addWidget(self.edit_button) + buttons_layout.addWidget(self.sales_button) + buttons_layout.addWidget(self.discount_button) + buttons_layout.addStretch() + + self.control_buttons.setLayout(buttons_layout) + layout.addWidget(self.control_buttons) + + # Добавляем растягивающийся элемент в конец + layout.addStretch() + + panel.setLayout(layout) + return panel + + def load_partners(self): + """Загрузка списка партнеров из API с авторизацией""" + try: + response = requests.get( + "http://localhost:8000/api/v1/partners", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.partners_list.clear() + partners = response.json() + + for partner in partners: + item = QListWidgetItem() + card = PartnerCard(partner) + card.partner_clicked.connect(self.show_partner_details) + + # Устанавливаем фиксированный размер для элемента + item.setSizeHint(card.sizeHint()) + self.partners_list.addItem(item) + self.partners_list.setItemWidget(item, card) + + # Сбрасываем выделение + self.partners_list.clearSelection() + self.current_partner = None + self.details_title.setText("Выберите партнера") + self.details_frame.hide() + self.edit_button.hide() + self.sales_button.hide() + self.discount_button.hide() + + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + else: + QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}") + + def show_partner_details(self, partner_data): + """Отображение детальной информации о партнере""" + self.current_partner = partner_data + self.details_title.setText(partner_data['company_name']) + + # Создаем новый виджет для деталей вместо очистки layout + new_details_frame = QFrame() + new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + new_details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + new_details_layout = QVBoxLayout() + new_details_layout.setSpacing(8) + + # Добавляем новую информацию + details = [ + ("Тип:", partner_data.get('partner_type', 'Не указан')), + ("ИНН:", partner_data.get('inn', 'Не указан')), + ("Директор:", partner_data.get('director_name', 'Не указан')), + ("Телефон:", partner_data.get('phone', 'Не указан')), + ("Email:", partner_data.get('email', 'Не указан')), + ("Рейтинг:", str(partner_data.get('rating', 0))), + ("Адрес:", partner_data.get('legal_address', 'Не указан')), + ("Регионы:", partner_data.get('sales_locations', 'Не указан')) + ] + + for label, value in details: + row_widget = QWidget() + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(0, 2, 0, 2) + + label_widget = QLabel(label) + label_widget.setStyleSheet("font-weight: bold; min-width: 100px;") + label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + value_widget = QLabel(str(value)) + value_widget.setWordWrap(True) + value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + row_layout.addWidget(label_widget) + row_layout.addWidget(value_widget) + row_layout.addStretch() + + new_details_layout.addWidget(row_widget) + + new_details_frame.setLayout(new_details_layout) + + # Заменяем старый details_frame на новый + old_frame = self.details_frame + layout = self.right_panel.layout() + layout.replaceWidget(old_frame, new_details_frame) + old_frame.deleteLater() + + self.details_frame = new_details_frame + self.details_layout = new_details_layout + + self.details_frame.show() + self.edit_button.show() + self.sales_button.show() + self.discount_button.show() + + def show_add_partner_form(self): + """Открытие формы добавления партнера""" + form = PartnerForm(self, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def edit_partner(self): + """Редактирование выбранного партнера""" + if self.current_partner: + form = PartnerForm(self, self.current_partner, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def show_sales_history(self): + """Открытие истории продаж партнера""" + if self.current_partner: + sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth) + sales_window.exec() + + def calculate_discount(self): + """Расчет скидки для партнера с авторизацией""" + if self.current_partner: + try: + response = requests.get( + f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + discount_data = response.json() + QMessageBox.information( + self, + "Расчет скидки", + f"Партнер: {self.current_partner['company_name']}\n" + f"Общие продажи: {discount_data['total_sales']}\n" + f"Скидка: {discount_data['discount_percent']}%" + ) + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}") + + def show_material_calculator(self): + """Открытие калькулятора материалов""" + calculator = MaterialCalculatorWindow(self, auth=self.auth) + calculator.exec() + + def logout(self): + """Выход из системы""" + reply = QMessageBox.question( + self, + "Подтверждение выхода", + "Вы уверены, что хотите выйти из системы?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + self.close() + # Здесь можно добавить вызов окна авторизации + # или перезапуск приложения + + def show_about(self): + """Показать информацию о программе""" + QMessageBox.about( + self, + "О программе MasterPol", + "MasterPol - Система управления партнерами\n\n" + "Версия: 1.0.0\n" + "Разработчик: Команда MasterPol\n\n" + "Система предназначена для управления партнерами,\n" + "учета продаж и расчета бизнес-показателей." + ) diff --git a/ressult/gui/main_window.py b/ressult/gui/main_window.py new file mode 100644 index 0000000..8af89e8 --- /dev/null +++ b/ressult/gui/main_window.py @@ -0,0 +1,574 @@ +# gui/main_window.py +""" +Главное окно приложения PyQt6 с поддержкой авторизации +""" +import sys +import requests +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QLabel, QPushButton, QListWidget, + QListWidgetItem, QMessageBox, QFrame, QStackedWidget, + QMenuBar, QMenu, QStatusBar, QToolBar) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction +from .partner_form import PartnerForm +from .sales_history import SalesHistoryWindow +from .material_calculator import MaterialCalculatorWindow + +class PartnerCard(QFrame): + """Карточка партнера для отображения в списке""" + partner_clicked = pyqtSignal(dict) + + def __init__(self, partner_data): + super().__init__() + self.partner_data = partner_data + self.setup_ui() + + def setup_ui(self): + self.setFrameStyle(QFrame.Shape.StyledPanel) + self.setStyleSheet(""" + PartnerCard { + background-color: white; + border: 1px solid #ddd; + border-radius: 8px; + padding: 12px; + margin: 4px; + } + PartnerCard:hover { + background-color: #f5f5f5; + border-color: #007acc; + } + """) + + layout = QVBoxLayout() + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(4) + + # Заголовок с типом и названием + header_layout = QHBoxLayout() + header_layout.setSpacing(4) + + type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |") + type_label.setStyleSheet("color: #666; font-weight: bold;") + + name_label = QLabel(self.partner_data['company_name']) + name_label.setStyleSheet("font-weight: bold; font-size: 14px;") + name_label.setWordWrap(True) + + # Безопасное преобразование рейтинга + rating_value = self.partner_data.get('rating', 0) + if isinstance(rating_value, float): + rating_value = int(rating_value) + + rating_label = QLabel(f"{rating_value}%") + rating_label.setStyleSheet("color: #007acc; font-weight: bold;") + + header_layout.addWidget(type_label) + header_layout.addWidget(name_label) + header_layout.addStretch() + header_layout.addWidget(rating_label) + + # Информация о директоре + director_label = QLabel(self.partner_data.get('director_name', 'Директор не указан')) + director_label.setStyleSheet("color: #444;") + + # Контактная информация + phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан')) + phone_label.setStyleSheet("color: #666;") + + layout.addLayout(header_layout) + layout.addWidget(director_label) + layout.addWidget(phone_label) + + self.setLayout(layout) + + def mousePressEvent(self, event): + """Обработка клика на карточке""" + if event.button() == Qt.MouseButton.LeftButton: + self.partner_clicked.emit(self.partner_data) + +class MainWindow(QMainWindow): + """Главное окно приложения с поддержкой авторизации""" + + def __init__(self, user_data): + super().__init__() + self.user_data = user_data + self.current_partner = None + self.auth = user_data.get('auth') + self.setup_ui() + self.load_partners() + + def setup_ui(self): + """Настройка интерфейса главного окна""" + self.setWindowTitle(f"MasterPol - Система управления партнерами") + self.setGeometry(100, 100, 1200, 700) + + # Установка иконки приложения + try: + self.setWindowIcon(QIcon("resources/icon.png")) + except: + pass + + # Создание меню + self.create_menu() + + # Создание тулбара + self.create_toolbar() + + # Создание статусной строки + self.create_statusbar() + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + main_layout = QHBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + + # Левая панель - список партнеров + left_panel = self.create_partners_panel() + main_layout.addWidget(left_panel, 1) + + # Правая панель - детальная информация + self.right_panel = self.create_details_panel() + main_layout.addWidget(self.right_panel, 2) + + central_widget.setLayout(main_layout) + + def create_menu(self): + """Создание меню приложения""" + menubar = self.menuBar() + + # Меню Файл + file_menu = menubar.addMenu('Файл') + + refresh_action = QAction('Обновить', self) + refresh_action.setShortcut('F5') + refresh_action.triggered.connect(self.load_partners) + file_menu.addAction(refresh_action) + + file_menu.addSeparator() + + logout_action = QAction('Выход', self) + logout_action.setShortcut('Ctrl+Q') + logout_action.triggered.connect(self.logout) + file_menu.addAction(logout_action) + + # Меню Сервис + service_menu = menubar.addMenu('Сервис') + + calc_action = QAction('Калькулятор материалов', self) + calc_action.triggered.connect(self.show_material_calculator) + service_menu.addAction(calc_action) + + # Меню Справка + help_menu = menubar.addMenu('Справка') + + about_action = QAction('О программе', self) + about_action.triggered.connect(self.show_about) + help_menu.addAction(about_action) + + def create_toolbar(self): + """Создание панели инструментов""" + toolbar = QToolBar("Основные инструменты") + self.addToolBar(toolbar) + + refresh_action = QAction('Обновить', self) + refresh_action.triggered.connect(self.load_partners) + toolbar.addAction(refresh_action) + + toolbar.addSeparator() + + add_partner_action = QAction('Добавить партнера', self) + add_partner_action.triggered.connect(self.show_add_partner_form) + toolbar.addAction(add_partner_action) + + def create_statusbar(self): + """Создание статусной строки""" + statusbar = self.statusBar() + user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}" + statusbar.showMessage(user_info) + + def create_partners_panel(self): + """Создание панели списка партнеров""" + panel = QWidget() + panel.setMaximumWidth(400) + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок + title = QLabel("Партнеры") + title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("padding: 10px;") + layout.addWidget(title) + + # Панель управления + control_layout = QHBoxLayout() + control_layout.setSpacing(10) + + self.add_button = QPushButton("Добавить партнера") + self.add_button.clicked.connect(self.show_add_partner_form) + self.add_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.refresh_button = QPushButton("Обновить") + self.refresh_button.clicked.connect(self.load_partners) + self.refresh_button.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #545b62; + } + """) + + control_layout.addWidget(self.add_button) + control_layout.addWidget(self.refresh_button) + control_layout.addStretch() + + layout.addLayout(control_layout) + + # Список партнеров + self.partners_list = QListWidget() + self.partners_list.setStyleSheet(""" + QListWidget { + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + outline: none; + } + QListWidget::item { + border: none; + padding: 0px; + } + QListWidget::item:selected { + background-color: transparent; + } + """) + layout.addWidget(self.partners_list) + + # Кнопка расчета материалов + self.calc_button = QPushButton("Калькулятор материалов") + self.calc_button.clicked.connect(self.show_material_calculator) + self.calc_button.setStyleSheet(""" + QPushButton { + background-color: #17a2b8; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #138496; + } + """) + layout.addWidget(self.calc_button) + + panel.setLayout(layout) + return panel + + def create_details_panel(self): + """Создание панели детальной информации""" + panel = QWidget() + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок детальной информации + self.details_title = QLabel("Выберите партнера") + self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold)) + self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.details_title.setStyleSheet("padding: 10px;") + layout.addWidget(self.details_title) + + # Детальная информация о партнере - создаем пустой frame + self.details_frame = QFrame() + self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + self.details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + self.details_layout = QVBoxLayout() + self.details_layout.setSpacing(8) + self.details_frame.setLayout(self.details_layout) + self.details_frame.hide() + + layout.addWidget(self.details_frame) + + # Кнопки управления выбранным партнером + self.control_buttons = QWidget() + buttons_layout = QHBoxLayout() + buttons_layout.setSpacing(10) + + self.edit_button = QPushButton("Редактировать") + self.edit_button.clicked.connect(self.edit_partner) + self.edit_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + self.edit_button.hide() + + self.sales_button = QPushButton("История продаж") + self.sales_button.clicked.connect(self.show_sales_history) + self.sales_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + self.sales_button.hide() + + self.discount_button = QPushButton("Расчет скидки") + self.discount_button.clicked.connect(self.calculate_discount) + self.discount_button.setStyleSheet(""" + QPushButton { + background-color: #ffc107; + color: black; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #e0a800; + } + """) + self.discount_button.hide() + + buttons_layout.addWidget(self.edit_button) + buttons_layout.addWidget(self.sales_button) + buttons_layout.addWidget(self.discount_button) + buttons_layout.addStretch() + + self.control_buttons.setLayout(buttons_layout) + layout.addWidget(self.control_buttons) + + # Добавляем растягивающийся элемент в конец + layout.addStretch() + + panel.setLayout(layout) + return panel + + def load_partners(self): + """Загрузка списка партнеров из API с авторизацией""" + try: + response = requests.get( + "http://localhost:8000/api/v1/partners", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.partners_list.clear() + partners = response.json() + + for partner in partners: + item = QListWidgetItem() + card = PartnerCard(partner) + card.partner_clicked.connect(self.show_partner_details) + + # Устанавливаем фиксированный размер для элемента + item.setSizeHint(card.sizeHint()) + self.partners_list.addItem(item) + self.partners_list.setItemWidget(item, card) + + # Сбрасываем выделение + self.partners_list.clearSelection() + self.current_partner = None + self.details_title.setText("Выберите партнера") + self.details_frame.hide() + self.edit_button.hide() + self.sales_button.hide() + self.discount_button.hide() + + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + else: + QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}") + + def show_partner_details(self, partner_data): + """Отображение детальной информации о партнере""" + self.current_partner = partner_data + self.details_title.setText(partner_data['company_name']) + + # Создаем новый виджет для деталей вместо очистки layout + new_details_frame = QFrame() + new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + new_details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + new_details_layout = QVBoxLayout() + new_details_layout.setSpacing(8) + + # Добавляем новую информацию + details = [ + ("Тип:", partner_data.get('partner_type', 'Не указан')), + ("ИНН:", partner_data.get('inn', 'Не указан')), + ("Директор:", partner_data.get('director_name', 'Не указан')), + ("Телефон:", partner_data.get('phone', 'Не указан')), + ("Email:", partner_data.get('email', 'Не указан')), + ("Рейтинг:", str(partner_data.get('rating', 0))), + ("Адрес:", partner_data.get('legal_address', 'Не указан')), + ("Регионы:", partner_data.get('sales_locations', 'Не указан')) + ] + + for label, value in details: + row_widget = QWidget() + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(0, 2, 0, 2) + + label_widget = QLabel(label) + label_widget.setStyleSheet("font-weight: bold; min-width: 100px;") + label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + value_widget = QLabel(str(value)) + value_widget.setWordWrap(True) + value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + row_layout.addWidget(label_widget) + row_layout.addWidget(value_widget) + row_layout.addStretch() + + new_details_layout.addWidget(row_widget) + + new_details_frame.setLayout(new_details_layout) + + # Заменяем старый details_frame на новый + old_frame = self.details_frame + layout = self.right_panel.layout() + layout.replaceWidget(old_frame, new_details_frame) + old_frame.deleteLater() + + self.details_frame = new_details_frame + self.details_layout = new_details_layout + + self.details_frame.show() + self.edit_button.show() + self.sales_button.show() + self.discount_button.show() + + def show_add_partner_form(self): + """Открытие формы добавления партнера""" + form = PartnerForm(self, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def edit_partner(self): + """Редактирование выбранного партнера""" + if self.current_partner: + form = PartnerForm(self, self.current_partner, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def show_sales_history(self): + """Открытие истории продаж партнера""" + if self.current_partner: + sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth) + sales_window.exec() + + def calculate_discount(self): + """Расчет скидки для партнера с авторизацией""" + if self.current_partner: + try: + response = requests.get( + f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + discount_data = response.json() + QMessageBox.information( + self, + "Расчет скидки", + f"Партнер: {self.current_partner['company_name']}\n" + f"Общие продажи: {discount_data['total_sales']}\n" + f"Скидка: {discount_data['discount_percent']}%" + ) + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}") + + def show_material_calculator(self): + """Открытие калькулятора материалов""" + calculator = MaterialCalculatorWindow(self, auth=self.auth) + calculator.exec() + + def logout(self): + """Выход из системы""" + reply = QMessageBox.question( + self, + "Подтверждение выхода", + "Вы уверены, что хотите выйти из системы?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + self.close() + # Здесь можно добавить вызов окна авторизации + # или перезапуск приложения + + def show_about(self): + """Показать информацию о программе""" + QMessageBox.about( + self, + "О программе MasterPol", + "MasterPol - Система управления партнерами\n\n" + "Версия: 1.0.0\n" + "Разработчик: Команда MasterPol\n\n" + "Система предназначена для управления партнерами,\n" + "учета продаж и расчета бизнес-показателей." + ) diff --git a/ressult/gui/main_window.py.bak b/ressult/gui/main_window.py.bak new file mode 100644 index 0000000..2051605 --- /dev/null +++ b/ressult/gui/main_window.py.bak @@ -0,0 +1,616 @@ +# gui/main_wind/w.py +""" +Главное окно приложения PyQt6 с поддержкой авторизации +""" +import sys +import requests +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QLabel, QPushButton, QListWidget, + QListWidgetItem, QMessageBox, QFrame, QStackedWidget, + QMenuBar, QMenu, QStatusBar, QToolBar) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction +from .partner_form import PartnerForm +from .sales_history import SalesHistoryWindow +from .material_calculator import MaterialCalculatorWindow + +class PartnerCard(QFrame): + """Карточка партнера для отображения в списке""" + partner_clicked = pyqtSignal(dict) + + def __init__(self, partner_data): + super().__init__() + self.partner_data = partner_data + self.setup_ui() + + def setup_ui(self): + self.setFrameStyle(QFrame.Shape.StyledPanel) + self.setStyleSheet(""" + PartnerCard { + background-color: white; + border: 1px solid #ddd; + border-radius: 8px; + padding: 12px; + margin: 4px; + } + PartnerCard:hover { + background-color: #f5f5f5; + border-color: #007acc; + } + """) + + layout = QVBoxLayout() + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(4) + + # Заголовок с типом и названием + header_layout = QHBoxLayout() + header_layout.setSpacing(4) + + type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |") + type_label.setStyleSheet("color: #666; font-weight: bold;") + + name_label = QLabel(self.partner_data['company_name']) + name_label.setStyleSheet("font-weight: bold; font-size: 14px;") + name_label.setWordWrap(True) + + # Безопасное преобразование рейтинга + rating_value = self.partner_data.get('rating', 0) + if isinstance(rating_value, float): + rating_value = int(rating_value) + + rating_label = QLabel(f"{rating_value}%") + rating_label.setStyleSheet("color: #007acc; font-weight: bold;") + + header_layout.addWidget(type_label) + header_layout.addWidget(name_label) + header_layout.addStretch() + header_layout.addWidget(rating_label) + + # Информация о директоре + QLabel(self.partner_data.get('director_name', 'Директор не указан')) + director_label.setStyleSheet("color: #444;") + + # Контактная информация + phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан')) + phone_label.setStyleSheet("color: #666;") + + layout.addLayout(header_layout) + layout.addWidget(director_label) + layout.addWidget(phone_label) + + self.setLayout(layout) + + def mousePressEvent(self, event): + """Обработка клика на карточке""" + if event.button() == Qt.MouseButton.LeftButton: + self.partner_clicked.emit(self.partner_data) + +class MainWindow(QMainWindow): + """Главное окно приложения с поддержкой авторизации""" + + def __init__(self, user_data): + super().__init__() + self.user_data = user_data + self.current_partner = None + self.orders_panel = None + self.auth = user_data.get('auth') + self.setup_ui() + self.load_partners() + + def setup_ui(self): + """Настройка интерфейса главного окна""" + self.setWindowTitle(f"MasterPol - Система управления партнерами") + self.setGeometry(100, 100, 1200, 700) + + # Установка иконки приложения + try: + self.setWindowIcon(QIcon("resources/icon.png")) + except: + pass + + # Создание меню + self.create_menu() + + # Создание тулбара + self.create_toolbar() + + # Создание статусной строки + self.create_statusbar() + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + main_layout = QHBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + + # Левая панель - список партнеров + left_panel = self.create_partners_panel() + main_layout.addWidget(left_panel, 1) + + # Правая панель - детальная информация + self.right_panel = self.create_details_panel() + main_layout.addWidget(self.right_panel, 2) + + central_widget.setLayout(main_layout) + + def create_menu(self): + """Создание меню приложения""" + menubar = self.menuBar() + + # Меню Файл + file_menu = menubar.addMenu('Файл') + + refresh_action = QAction('Обновить', self) + refresh_action.setShortcut('F5') + refresh_action.triggered.connect(self.load_partners) + file_menu.addAction(refresh_action) + + file_menu.addSeparator() + + logout_action = QAction('Выход', self) + logout_action.setShortcut('Ctrl+Q') + logout_action.triggered.connect(self.logout) + file_menu.addAction(logout_action) + + # Меню Сервис + service_menu = menubar.addMenu('Сервис') + + calc_action = QAction('Калькулятор материалов', self) + calc_action.triggered.connect(self.show_material_calculator) + service_menu.addAction(calc_action) + + # Меню Справка + help_menu = menubar.addMenu('Справка') + + about_action = QAction('О программе', self) + about_action.triggered.connect(self.show_about) + help_menu.addAction(about_action) + + def create_toolbar(self): + """Создание панели инструментов""" + toolbar = QToolBar("Основные инструменты") + self.addToolBar(toolbar) + + refresh_action = QAction('Обновить', self) + refresh_action.triggered.connect(self.load_partners) + toolbar.addAction(refresh_action) + + toolbar.addSeparator() + + add_partner_action = QAction('Добавить партнера', self) + add_partner_action.triggered.connect(self.show_add_partner_form) + toolbar.addAction(add_partner_action) + + def create_statusbar(self): + """Создание статусной строки""" + statusbar = self.statusBar() + user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}" + statusbar.showMessage(user_info) + + def create_partners_panel(self): + """Создание панели списка партнеров""" + panel = QWidget() + panel.setMaximumWidth(400) + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок + title = QLabel("Партнеры") + title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("padding: 10px;") + layout.addWidget(title) + + # Панель управления + control_layout = QHBoxLayout() + control_layout.setSpacing(10) + + self.add_button = QPushButton("Добавить партнера") + self.add_button.clicked.connect(self.show_add_partner_form) + self.add_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.refresh_button = QPushButton("Обновить") + self.refresh_button.clicked.connect(self.load_partners) + self.refresh_button.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #545b62; + } + """) + + control_layout.addWidget(self.add_button) + control_layout.addWidget(self.refresh_button) + control_layout.addStretch() + + layout.addLayout(control_layout) + + # Список партнеров + self.partners_list = QListWidget() + self.partners_list.setStyleSheet(""" + QListWidget { + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + outline: none; + } + QListWidget::item { + border: none; + padding: 0px; + } + QListWidget::item:selected { + background-color: transparent; + } + """) + layout.addWidget(self.partners_list) + + # Кнопка расчета материалов + self.calc_button = QPushButton("Калькулятор материалов") + self.calc_button.clicked.connect(self.show_material_calculator) + self.calc_button.setStyleSheet(""" + QPushButton { + background-color: #17a2b8; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #138496; + } + """) + layout.addWidget(self.calc_button) + + panel.setLayout(layout) + return panel + + def create_details_panel(self): + """Создание панели детальной информации""" + panel = QWidget() + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок детальной информации + self.details_title = QLabel("Выберите партнера") + self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold)) + self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.details_title.setStyleSheet("padding: 10px;") + layout.addWidget(self.details_title) + + # Детальная информация о партнере - создаем пустой frame + self.details_frame = QFrame() + self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + self.details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + self.details_layout = QVBoxLayout() + self.details_layout.setSpacing(8) + self.details_frame.setLayout(self.details_layout) + self.details_frame.hide() + + layout.addWidget(self.details_frame) + + # Кнопки управления выбранным партнером + self.control_buttons = QWidget() + buttons_layout = QHBoxLayout() + buttons_layout.setSpacing(10) + + self.edit_button = QPushButton("Редактировать") + self.edit_button.clicked.connect(self.edit_partner) + self.edit_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + self.edit_button.hide() + + self.sales_button = QPushButton("История продаж") + self.sales_button.clicked.connect(self.show_sales_history) + self.sales_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + self.sales_button.hide() + + self.discount_button = QPushButton("Расчет скидки") + self.discount_button.clicked.connect(self.calculate_discount) + self.discount_button.setStyleSheet(""" + QPushButton { + background-color: #ffc107; + color: black; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #e0a800; + } + """) + self.discount_button.hide() + + buttons_layout.addWidget(self.edit_button) + buttons_layout.addWidget(self.sales_button) + buttons_layout.addWidget(self.discount_button) + buttons_layout.addStretch() + + self.control_buttons.setLayout(buttons_layout) + layout.addWidget(self.control_buttons) + + # Добавляем растягивающийся элемент в конец + layout.addStretch() + + panel.setLayout(layout) + return panel + + def load_partners(self): + """Загрузка списка партнеров из API с авторизацией""" + try: + response = requests.get( + "http://localhost:8000/api/v1/partners", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.partners_list.clear() + partners = response.json() + + for partner in partners: + item = QListWidgetItem() + card = PartnerCard(partner) + card.partner_clicked.connect(self.show_partner_details) + + # Устанавливаем фиксированный размер для элемента + item.setSizeHint(card.sizeHint()) + self.partners_list.addItem(item) + self.partners_list.setItemWidget(item, card) + + # Сбрасываем выделение + self.partners_list.clearSelection() + self.current_partner = None + self.details_title.setText("Выберите партнера") + self.details_frame.hide() + self.edit_button.hide() + self.sales_button.hide() + self.discount_button.hide() + + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + else: + QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}") + + def show_partner_details(self, partner_data): + """Отображение детальной информации о партнере""" + self.current_partner = partner_data + self.details_title.setText(partner_data['company_name']) + + # Создаем новый виджет для деталей вместо очистки layout + new_details_frame = QFrame() + new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel) + new_details_frame.setStyleSheet(""" + QFrame { + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px; + } + """) + new_details_layout = QVBoxLayout() + new_details_layout.setSpacing(8) + + # Добавляем новую информацию + details = [ + ("Тип:", partner_data.get('partner_type', 'Не указан')), + ("ИНН:", partner_data.get('inn', 'Не указан')), + ("Директор:", partner_data.get('director_name', 'Не указан')), + ("Телефон:", partner_data.get('phone', 'Не указан')), + ("Email:", partner_data.get('email', 'Не указан')), + ("Рейтинг:", str(partner_data.get('rating', 0))), + ("Адрес:", partner_data.get('legal_address', 'Не указан')), + ("Регионы:", partner_data.get('sales_locations', 'Не указан')) + ] + + # ЗАМЕНИТЕ этот блок кода в методе show_partner_details: +for label, value in details: + row_widget = QWidget() + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(0, 2, 0, 2) + + label_widget = QLabel(label) + label_widget.setStyleSheet("font-weight: bold; min-width: 100px;") + label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + value_widget = QLabel(str(value)) + value_widget.setWordWrap(True) + value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + + row_layout.addWidget(label_widget) + row_layout.addWidget(value_widget) + row_layout.addStretch() + + new_details_layout.addWidget(row_widget) + + # НА этот исправленный вариант: + for label, value in details: + # Создаем контейнер для строки + row_container = QWidget() + row_container.setFixedHeight(30) # Фиксированная высота для каждой строки + row_layout = QHBoxLayout(row_container) + row_layout.setContentsMargins(5, 0, 5, 0) + row_layout.setSpacing(10) + + # Лейбл (название поля) + label_widget = QLabel(label) + label_widget.setStyleSheet(""" + QLabel { + font-weight: bold; + color: #333; + min-width: 120px; + max-width: 120px; + background-color: transparent; + } + """) + label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + + # Значение + value_widget = QLabel(str(value)) + value_widget.setStyleSheet(""" + QLabel { + color: #555; + background-color: transparent; + border: none; + } + """) + value_widget.setWordWrap(True) + value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + + row_layout.addWidget(label_widget) + row_layout.addWidget(value_widget) + row_layout.addStretch() + + new_details_layout.addWidget(row_container) + + new_details_frame.setLayout(new_details_layout) + + # Заменяем старый details_frame на новый + old_frame = self.details_frame + layout = self.right_panel.layout() + layout.replaceWidget(old_frame, new_details_frame) + old_frame.deleteLater() + + self.details_frame = new_details_frame + self.details_layout = new_details_layout + + self.details_frame.show() + self.edit_button.show() + self.sales_button.show() + self.discount_button.show() + + def show_add_partner_form(self): + """Открытие формы добавления партнера""" + form = PartnerForm(self, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def edit_partner(self): + """Редактирование выбранного партнера""" + if self.current_partner: + form = PartnerForm(self, self.current_partner, auth=self.auth) + form.partner_saved.connect(self.load_partners) + form.exec() + + def show_sales_history(self): + """Открытие истории продаж партнера""" + if self.current_partner: + sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth) + sales_window.exec() + + def calculate_discount(self): + """Расчет скидки для партнера с авторизацией""" + if self.current_partner: + try: + response = requests.get( + f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount", + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + discount_data = response.json() + QMessageBox.information( + self, + "Расчет скидки", + f"Партнер: {self.current_partner['company_name']}\n" + f"Общие продажи: {discount_data['total_sales']}\n" + f"Скидка: {discount_data['discount_percent']}%" + ) + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + self.logout() + + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}") + + def show_material_calculator(self): + """Открытие калькулятора материалов""" + calculator = MaterialCalculatorWindow(self, auth=self.auth) + calculator.exec() + + def logout(self): + """Выход из системы""" + reply = QMessageBox.question( + self, + "Подтверждение выхода", + "Вы уверены, что хотите выйти из системы?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + self.close() + # Здесь можно добавить вызов окна авторизации + # или перезапуск приложения + + def show_about(self): + """Показать информацию о программе""" + QMessageBox.about( + self, + "О программе MasterPol", + "MasterPol - Система управления партнерами\n\n" + "Версия: 1.0.0\n" + "Разработчик: Команда MasterPol\n\n" + "Система предназначена для управления партнерами,\n" + "учета продаж и расчета бизнес-показателей." + ) diff --git a/ressult/gui/material_calculator.py b/ressult/gui/material_calculator.py new file mode 100644 index 0000000..6903972 --- /dev/null +++ b/ressult/gui/material_calculator.py @@ -0,0 +1,160 @@ +# gui/material_calculator.py +""" +Калькулятор материалов для производства +Соответствует модулю 4 ТЗ +""" +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QMessageBox, QFormLayout, + QDoubleSpinBox, QSpinBox) +from PyQt6.QtCore import Qt +import requests +import math + +class MaterialCalculatorWindow(QDialog): + def __init__(self, parent=None, auth=None): + super().__init__(parent) + self.auth = auth # Сохраняем auth, даже если не используется + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Калькулятор материалов для производства") + self.setModal(True) + self.resize(400, 300) + + layout = QVBoxLayout() + + # Заголовок + title = QLabel("Расчет количества материала") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;") + layout.addWidget(title) + + # Форма ввода параметров + form_layout = QFormLayout() + + self.product_type_id = QSpinBox() + self.product_type_id.setRange(1, 100) + form_layout.addRow("ID типа продукции:", self.product_type_id) + + self.material_type_id = QSpinBox() + self.material_type_id.setRange(1, 100) + form_layout.addRow("ID типа материала:", self.material_type_id) + + self.quantity = QSpinBox() + self.quantity.setRange(1, 1000000) + form_layout.addRow("Количество продукции:", self.quantity) + + self.param1 = QDoubleSpinBox() + self.param1.setRange(0.1, 1000.0) + self.param1.setDecimals(2) + form_layout.addRow("Параметр продукции 1:", self.param1) + + self.param2 = QDoubleSpinBox() + self.param2.setRange(0.1, 1000.0) + self.param2.setDecimals(2) + form_layout.addRow("Параметр продукции 2:", self.param2) + + self.product_coeff = QDoubleSpinBox() + self.product_coeff.setRange(0.1, 10.0) + self.product_coeff.setDecimals(3) + self.product_coeff.setValue(1.0) + form_layout.addRow("Коэффициент типа продукции:", self.product_coeff) + + self.defect_percent = QDoubleSpinBox() + self.defect_percent.setRange(0.0, 50.0) + self.defect_percent.setDecimals(1) + self.defect_percent.setSuffix("%") + form_layout.addRow("Процент брака материала:", self.defect_percent) + + layout.addLayout(form_layout) + + # Результат + self.result_label = QLabel() + self.result_label.setStyleSheet("font-weight: bold; color: #007acc; margin: 10px;") + self.result_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.result_label) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.calculate_button = QPushButton("Рассчитать") + self.calculate_button.clicked.connect(self.calculate_material) + self.calculate_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.close_button = QPushButton("Закрыть") + self.close_button.clicked.connect(self.accept) + + buttons_layout.addWidget(self.calculate_button) + buttons_layout.addStretch() + buttons_layout.addWidget(self.close_button) + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + def calculate_material(self): + """Расчет количества материала с обработкой ошибок""" + try: + # Проверяем валидность входных данных + if (self.param1.value() <= 0 or self.param2.value() <= 0 or + self.product_coeff.value() <= 0 or self.defect_percent.value() < 0): + self.result_label.setText("Ошибка: неверные параметры") + self.result_label.setStyleSheet("color: red; font-weight: bold;") + return + + # Создаем данные для расчета + calculation_data = { + 'product_type_id': self.product_type_id.value(), + 'material_type_id': self.material_type_id.value(), + 'quantity': self.quantity.value(), + 'param1': self.param1.value(), + 'param2': self.param2.value(), + 'product_coeff': self.product_coeff.value(), + 'defect_percent': self.defect_percent.value() + } + + # Локальный расчет (без API) + material_quantity = self.calculate_locally(calculation_data) + + if material_quantity >= 0: + self.result_label.setText( + f"Необходимое количество материала: {material_quantity} единиц" + ) + self.result_label.setStyleSheet("color: #007acc; font-weight: bold;") + else: + self.result_label.setText("Ошибка: неверные параметры расчета") + self.result_label.setStyleSheet("color: red; font-weight: bold;") + + except Exception as e: + self.result_label.setText(f"Ошибка расчета: {str(e)}") + self.result_label.setStyleSheet("color: red; font-weight: bold;") + + def calculate_locally(self, data): + """Локальный расчет материалов""" + try: + import math + + # Расчет количества материала на одну единицу продукции + material_per_unit = data['param1'] * data['param2'] * data['product_coeff'] + + # Расчет общего количества материала с учетом брака + total_material = material_per_unit * data['quantity'] + total_material_with_defect = total_material * (1 + data['defect_percent'] / 100) + + # Округление до целого числа в большую сторону + return math.ceil(total_material_with_defect) + + except: + return -1 diff --git a/ressult/gui/orders_panel.py b/ressult/gui/orders_panel.py new file mode 100644 index 0000000..64576a3 --- /dev/null +++ b/ressult/gui/orders_panel.py @@ -0,0 +1,344 @@ +# gui/orders_panel.py +""" +Панель управления заказами и продажами +""" +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QTableWidget, QTableWidgetItem, QPushButton, + QHeaderView, QMessageBox, QDateEdit, QComboBox, + QLineEdit, QFormLayout, QDialog, QDoubleSpinBox) +from PyQt6.QtCore import Qt, QDate +from PyQt6.QtGui import QFont +import requests + +class OrderForm(QDialog): + """Форма для добавления/редактирования заказа""" + + order_saved = pyqtSignal() + + def __init__(self, parent=None, order_data=None, auth=None, partners=None): + super().__init__(parent) + self.order_data = order_data + self.auth = auth + self.partners = partners or [] + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Добавить заказ" if not self.order_data else "Редактировать заказ") + self.setModal(True) + self.resize(400, 300) + + layout = QVBoxLayout() + + # Форма ввода данных + form_layout = QFormLayout() + + # Выбор партнера + self.partner_combo = QComboBox() + self.partner_combo.addItem("Выберите партнера", None) + for partner in self.partners: + self.partner_combo.addItem(partner['company_name'], partner['partner_id']) + form_layout.addRow("Партнер*:", self.partner_combo) + + # Название продукта + self.product_name = QLineEdit() + self.product_name.setPlaceholderText("Введите название продукта") + form_layout.addRow("Продукт*:", self.product_name) + + # Количество + self.quantity = QDoubleSpinBox() + self.quantity.setRange(0.01, 100000.0) + self.quantity.setDecimals(2) + form_layout.addRow("Количество*:", self.quantity) + + # Дата продажи + self.sale_date = QDateEdit() + self.sale_date.setDate(QDate.currentDate()) + self.sale_date.setCalendarPopup(True) + form_layout.addRow("Дата продажи*:", self.sale_date) + + layout.addLayout(form_layout) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.save_button = QPushButton("Сохранить") + self.save_button.clicked.connect(self.save_order) + self.save_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + + self.cancel_button = QPushButton("Отмена") + self.cancel_button.clicked.connect(self.reject) + + buttons_layout.addWidget(self.save_button) + buttons_layout.addWidget(self.cancel_button) + buttons_layout.addStretch() + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + # Если редактирование, заполняем форму + if self.order_data: + self.fill_form() + + def fill_form(self): + """Заполнение формы данными заказа""" + data = self.order_data + + # Устанавливаем партнера + partner_index = self.partner_combo.findData(data.get('partner_id')) + if partner_index >= 0: + self.partner_combo.setCurrentIndex(partner_index) + + self.product_name.setText(data.get('product_name', '')) + self.quantity.setValue(float(data.get('quantity', 0))) + + # Устанавливаем дату + sale_date = data.get('sale_date') + if sale_date: + date = QDate.fromString(sale_date, 'yyyy-MM-dd') + if date.isValid(): + self.sale_date.setDate(date) + + def validate_form(self): + """Валидация данных формы""" + errors = [] + + if not self.partner_combo.currentData(): + errors.append("Выберите партнера") + + if not self.product_name.text().strip(): + errors.append("Введите название продукта") + + if self.quantity.value() <= 0: + errors.append("Количество должно быть больше 0") + + return errors + + def save_order(self): + """Сохранение заказа""" + errors = self.validate_form() + if errors: + QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors)) + return + + order_data = { + 'partner_id': self.partner_combo.currentData(), + 'product_name': self.product_name.text().strip(), + 'quantity': self.quantity.value(), + 'sale_date': self.sale_date.date().toString('yyyy-MM-dd') + } + + try: + if self.order_data: + # Обновление существующего заказа + response = requests.put( + f"http://localhost:8000/api/v1/sales/{self.order_data['sale_id']}", + json=order_data, + auth=self.auth, + timeout=10 + ) + else: + # Создание нового заказа + response = requests.post( + "http://localhost:8000/api/v1/sales", + json=order_data, + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.order_saved.emit() + QMessageBox.information(self, "Успех", "Заказ успешно сохранен") + self.accept() + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + else: + error_msg = response.json().get('detail', 'Неизвестная ошибка') + QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить заказ: {error_msg}") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}") + +class OrdersPanel(QWidget): + """Панель управления заказами""" + + def __init__(self, auth=None): + super().__init__() + self.auth = auth + self.partners = [] + self.setup_ui() + self.load_partners() + self.load_orders() + + def setup_ui(self): + """Настройка интерфейса панели заказов""" + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Заголовок + title = QLabel("Управление заказами") + title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(title) + + # Панель управления + control_layout = QHBoxLayout() + + self.add_button = QPushButton("Добавить заказ") + self.add_button.clicked.connect(self.show_add_order_form) + self.add_button.setStyleSheet(""" + QPushButton { + background-color: #007acc; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + self.refresh_button = QPushButton("Обновить") + self.refresh_button.clicked.connect(self.load_orders) + + control_layout.addWidget(self.add_button) + control_layout.addWidget(self.refresh_button) + control_layout.addStretch() + + layout.addLayout(control_layout) + + # Таблица заказов + self.orders_table = QTableWidget() + self.orders_table.setColumnCount(6) + self.orders_table.setHorizontalHeaderLabels([ + "ID", "Партнер", "Продукт", "Количество", "Дата", "Действия" + ]) + self.orders_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + self.orders_table.setStyleSheet(""" + QTableWidget { + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + } + QTableWidget::item { + padding: 8px; + } + """) + + layout.addWidget(self.orders_table) + + self.setLayout(layout) + + def load_partners(self): + """Загрузка списка партнеров""" + try: + response = requests.get( + "http://localhost:8000/api/v1/partners", + auth=self.auth, + timeout=10 + ) + if response.status_code == 200: + self.partners = response.json() + except: + self.partners = [] + + def load_orders(self): + """Загрузка списка заказов""" + try: + response = requests.get( + "http://localhost:8000/api/v1/sales", + auth=self.auth, + timeout=10 + ) + if response.status_code == 200: + orders = response.json() + self.display_orders(orders) + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить заказы: {str(e)}") + + def display_orders(self, orders): + """Отображение заказов в таблице""" + self.orders_table.setRowCount(len(orders)) + + for row, order in enumerate(orders): + self.orders_table.setItem(row, 0, QTableWidgetItem(str(order.get('sale_id', '')))) + self.orders_table.setItem(row, 1, QTableWidgetItem(order.get('company_name', 'Неизвестно'))) + self.orders_table.setItem(row, 2, QTableWidgetItem(order.get('product_name', ''))) + self.orders_table.setItem(row, 3, QTableWidgetItem(str(order.get('quantity', '')))) + self.orders_table.setItem(row, 4, QTableWidgetItem(order.get('sale_date', ''))) + + # Кнопки действий + actions_widget = QWidget() + actions_layout = QHBoxLayout(actions_widget) + actions_layout.setContentsMargins(4, 4, 4, 4) + actions_layout.setSpacing(4) + + delete_button = QPushButton("Удалить") + delete_button.setStyleSheet(""" + QPushButton { + background-color: #dc3545; + color: white; + border: none; + padding: 4px 8px; + border-radius: 3px; + font-size: 11px; + } + QPushButton:hover { + background-color: #c82333; + } + """) + delete_button.clicked.connect(lambda checked, o=order: self.delete_order(o)) + + actions_layout.addWidget(delete_button) + actions_layout.addStretch() + + self.orders_table.setCellWidget(row, 5, actions_widget) + + def show_add_order_form(self): + """Открытие формы добавления заказа""" + form = OrderForm(self, auth=self.auth, partners=self.partners) + form.order_saved.connect(self.load_orders) + form.exec() + + def delete_order(self, order): + """Удаление заказа""" + reply = QMessageBox.question( + self, + "Подтверждение удаления", + f"Вы уверены, что хотите удалить заказ #{order.get('sale_id')}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + try: + response = requests.delete( + f"http://localhost:8000/api/v1/sales/{order['sale_id']}", + auth=self.auth, + timeout=10 + ) + if response.status_code == 200: + self.load_orders() + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла") + except Exception as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось удалить заказ: {str(e)}") diff --git a/ressult/gui/partner_form.py b/ressult/gui/partner_form.py new file mode 100644 index 0000000..9d2310b --- /dev/null +++ b/ressult/gui/partner_form.py @@ -0,0 +1,193 @@ +# gui/partner_form.py (обновленный) +""" +Форма для добавления/редактирования партнера с поддержкой авторизации +""" +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QComboBox, QPushButton, QMessageBox, + QFormLayout, QSpinBox) +from PyQt6.QtCore import pyqtSignal +import requests + +class PartnerForm(QDialog): + partner_saved = pyqtSignal() + + def __init__(self, parent=None, partner_data=None, auth=None): + super().__init__(parent) + self.partner_data = partner_data + self.auth = auth + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Добавить партнера" if not self.partner_data else "Редактировать партнера") + self.setModal(True) + self.resize(500, 400) + + layout = QVBoxLayout() + + # Форма ввода данных + form_layout = QFormLayout() + + self.company_name = QLineEdit() + self.company_name.setPlaceholderText("Введите наименование компании") + form_layout.addRow("Наименование компании*:", self.company_name) + + self.inn = QLineEdit() + self.inn.setPlaceholderText("Введите ИНН") + form_layout.addRow("ИНН*:", self.inn) + + self.partner_type = QComboBox() + self.partner_type.addItems(["", "distributor", "retail", "wholesale", "dealer"]) + self.partner_type.setPlaceholderText("Выберите тип партнера") + form_layout.addRow("Тип партнера:", self.partner_type) + + self.rating = QSpinBox() + self.rating.setRange(0, 100) + self.rating.setSuffix("%") + form_layout.addRow("Рейтинг:", self.rating) + + self.legal_address = QLineEdit() + self.legal_address.setPlaceholderText("Введите юридический адрес") + form_layout.addRow("Юридический адрес:", self.legal_address) + + self.director_name = QLineEdit() + self.director_name.setPlaceholderText("Введите ФИО директора") + form_layout.addRow("ФИО директора:", self.director_name) + + self.phone = QLineEdit() + self.phone.setPlaceholderText("+7XXXXXXXXXX") + form_layout.addRow("Телефон:", self.phone) + + self.email = QLineEdit() + self.email.setPlaceholderText("email@example.com") + form_layout.addRow("Email:", self.email) + + self.sales_locations = QLineEdit() + self.sales_locations.setPlaceholderText("Москва, Санкт-Петербург...") + form_layout.addRow("Регионы продаж:", self.sales_locations) + + layout.addLayout(form_layout) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.save_button = QPushButton("Сохранить") + self.save_button.clicked.connect(self.save_partner) + self.save_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + + self.cancel_button = QPushButton("Отмена") + self.cancel_button.clicked.connect(self.reject) + + buttons_layout.addWidget(self.save_button) + buttons_layout.addWidget(self.cancel_button) + buttons_layout.addStretch() + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + # Если редактирование, заполняем форму + if self.partner_data: + self.fill_form() + + def fill_form(self): + """Заполнение формы данными партнера""" + data = self.partner_data + self.company_name.setText(data.get('company_name', '')) + self.inn.setText(data.get('inn', '')) + + partner_type = data.get('partner_type', '') + if partner_type: + index = self.partner_type.findText(partner_type) + if index >= 0: + self.partner_type.setCurrentIndex(index) + + # Безопасное преобразование рейтинга + rating = data.get('rating', 0) + if isinstance(rating, float): + rating = int(rating) + self.rating.setValue(rating) + + self.legal_address.setText(data.get('legal_address', '')) + self.director_name.setText(data.get('director_name', '')) + self.phone.setText(data.get('phone', '')) + self.email.setText(data.get('email', '')) + self.sales_locations.setText(data.get('sales_locations', '')) + + def validate_form(self): + """Валидация данных формы""" + errors = [] + + if not self.company_name.text().strip(): + errors.append("Наименование компании обязательно") + + if not self.inn.text().strip(): + errors.append("ИНН обязателен") + + if self.phone.text() and not self.phone.text().startswith('+'): + errors.append("Телефон должен начинаться с '+'") + + return errors + + def save_partner(self): + """Сохранение партнера с авторизацией""" + errors = self.validate_form() + if errors: + QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors)) + return + + partner_data = { + 'company_name': self.company_name.text().strip(), + 'inn': self.inn.text().strip(), + 'partner_type': self.partner_type.currentText() or None, + 'rating': self.rating.value(), + 'legal_address': self.legal_address.text().strip() or None, + 'director_name': self.director_name.text().strip() or None, + 'phone': self.phone.text().strip() or None, + 'email': self.email.text().strip() or None, + 'sales_locations': self.sales_locations.text().strip() or None + } + + try: + if self.partner_data: + # Обновление существующего партнера + response = requests.put( + f"http://localhost:8000/api/v1/partners/{self.partner_data['partner_id']}", + json=partner_data, + auth=self.auth, + timeout=10 + ) + else: + # Создание нового партнера + response = requests.post( + "http://localhost:8000/api/v1/partners", + json=partner_data, + auth=self.auth, + timeout=10 + ) + + if response.status_code == 200: + self.partner_saved.emit() + QMessageBox.information(self, "Успех", "Партнер успешно сохранен") + self.accept() + elif response.status_code == 401: + QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.") + else: + error_msg = response.json().get('detail', 'Неизвестная ошибка') + QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить партнера: {error_msg}") + + except requests.exceptions.ConnectionError: + QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}") diff --git a/ressult/gui/partner_form.py.bak b/ressult/gui/partner_form.py.bak new file mode 100644 index 0000000..da98b84 --- /dev/null +++ b/ressult/gui/partner_form.py.bak @@ -0,0 +1,186 @@ +# gui/partner_form.py +""" +Форма для добавления/редактирования партнера +Соответствует модулю 3 ТЗ +""" +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QComboBox, QPushButton, QMessageBox, + QFormLayout, QSpinBox) +from PyQt6.QtCore import pyqtSignal +import requests + +class PartnerForm(QDialog): + partner_saved = pyqtSignal() + + def __init__(self, parent=None, partner_data=None): + super().__init__(parent) + self.partner_data = partner_data + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Добавить партнера" if not self.partner_data else "Редактировать партнера") + self.setModal(True) + self.resize(500, 400) + + layout = QVBoxLayout() + + # Форма ввода данных + form_layout = QFormLayout() + + self.company_name = QLineEdit() + self.company_name.setPlaceholderText("Введите наименование компании") + form_layout.addRow("Наименование компании*:", self.company_name) + + self.inn = QLineEdit() + self.inn.setPlaceholderText("Введите ИНН") + form_layout.addRow("ИНН*:", self.inn) + + self.partner_type = QComboBox() + self.partner_type.addItems(["", "distributor", "retail", "wholesale", "dealer"]) + self.partner_type.setPlaceholderText("Выберите тип партнера") + form_layout.addRow("Тип партнера:", self.partner_type) + + self.rating = QSpinBox() + self.rating.setRange(0, 100) + self.rating.setSuffix("%") + form_layout.addRow("Рейтинг:", self.rating) + + self.legal_address = QLineEdit() + self.legal_address.setPlaceholderText("Введите юридический адрес") + form_layout.addRow("Юридический адрес:", self.legal_address) + + self.director_name = QLineEdit() + self.director_name.setPlaceholderText("Введите ФИО директора") + form_layout.addRow("ФИО директора:", self.director_name) + + self.phone = QLineEdit() + self.phone.setPlaceholderText("+7XXXXXXXXXX") + form_layout.addRow("Телефон:", self.phone) + + self.email = QLineEdit() + self.email.setPlaceholderText("email@example.com") + form_layout.addRow("Email:", self.email) + + self.sales_locations = QLineEdit() + self.sales_locations.setPlaceholderText("Москва, Санкт-Петербург...") + form_layout.addRow("Регионы продаж:", self.sales_locations) + + layout.addLayout(form_layout) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.save_button = QPushButton("Сохранить") + self.save_button.clicked.connect(self.save_partner) + self.save_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #218838; + } + """) + + self.cancel_button = QPushButton("Отмена") + self.cancel_button.clicked.connect(self.reject) + + buttons_layout.addWidget(self.save_button) + buttons_layout.addWidget(self.cancel_button) + buttons_layout.addStretch() + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + # Если редактирование, заполняем форму + if self.partner_data: + self.fill_form() + + # gui/partner_form.py (исправленный метод fill_form) + def fill_form(self): + """Заполнение формы данными партнера""" + data = self.partner_data + self.company_name.setText(data.get('company_name', '')) + self.inn.setText(data.get('inn', '')) + + partner_type = data.get('partner_type', '') + if partner_type: + index = self.partner_type.findText(partner_type) + if index >= 0: + self.partner_type.setCurrentIndex(index) + + # Безопасное преобразование рейтинга к int + rating = data.get('rating', 0) + if isinstance(rating, float): + rating = int(rating) + self.rating.setValue(rating) + + self.legal_address.setText(data.get('legal_address', '')) + self.director_name.setText(data.get('director_name', '')) + self.phone.setText(data.get('phone', '')) + self.email.setText(data.get('email', '')) + self.sales_locations.setText(data.get('sales_locations', '')) + + def validate_form(self): + """Валидация данных формы""" + errors = [] + + if not self.company_name.text().strip(): + errors.append("Наименование компании обязательно") + + if not self.inn.text().strip(): + errors.append("ИНН обязателен") + + if self.phone.text() and not self.phone.text().startswith('+'): + errors.append("Телефон должен начинаться с '+'") + + return errors + + def save_partner(self): + """Сохранение партнера""" + errors = self.validate_form() + if errors: + QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors)) + return + + partner_data = { + 'company_name': self.company_name.text().strip(), + 'inn': self.inn.text().strip(), + 'partner_type': self.partner_type.currentText() or None, + 'rating': self.rating.value(), + 'legal_address': self.legal_address.text().strip() or None, + 'director_name': self.director_name.text().strip() or None, + 'phone': self.phone.text().strip() or None, + 'email': self.email.text().strip() or None, + 'sales_locations': self.sales_locations.text().strip() or None + } + + try: + if self.partner_data: + # Обновление существующего партнера + response = requests.put( + f"http://localhost:8000/api/v1/partners/{self.partner_data['partner_id']}", + json=partner_data + ) + else: + # Создание нового партнера + response = requests.post( + "http://localhost:8000/api/v1/partners", + json=partner_data + ) + + if response.status_code == 200: + self.partner_saved.emit() + QMessageBox.information(self, "Успех", "Партнер успешно сохранен") + self.accept() + else: + error_msg = response.json().get('detail', 'Неизвестная ошибка') + QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить партнера: {error_msg}") + + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}") diff --git a/ressult/gui/sales_history.py b/ressult/gui/sales_history.py new file mode 100644 index 0000000..1c4c571 --- /dev/null +++ b/ressult/gui/sales_history.py @@ -0,0 +1,91 @@ +# gui/sales_history.py +""" +Окно истории продаж партнера +Соответствует модулю 4 ТЗ +""" +from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QTableWidget, QTableWidgetItem, QPushButton, + QHeaderView, QMessageBox) +from PyQt6.QtCore import Qt +import requests + +class SalesHistoryWindow(QDialog): + def __init__(self, partner_data, parent=None): + super().__init__(parent) + self.partner_data = partner_data + self.setup_ui() + self.load_sales_history() + + def setup_ui(self): + self.setWindowTitle(f"История продаж - {self.partner_data['company_name']}") + self.setModal(True) + self.resize(800, 400) + + layout = QVBoxLayout() + + # Заголовок + title = QLabel(f"История реализации продукции\n{self.partner_data['company_name']}") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;") + layout.addWidget(title) + + # Таблица продаж + self.sales_table = QTableWidget() + self.sales_table.setColumnCount(4) + self.sales_table.setHorizontalHeaderLabels([ + "ID", "Наименование продукции", "Количество", "Дата продажи" + ]) + self.sales_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + layout.addWidget(self.sales_table) + + # Статистика + self.stats_label = QLabel() + self.stats_label.setStyleSheet("font-weight: bold; margin: 10px;") + layout.addWidget(self.stats_label) + + # Кнопки + buttons_layout = QHBoxLayout() + + self.close_button = QPushButton("Закрыть") + self.close_button.clicked.connect(self.accept) + + buttons_layout.addStretch() + buttons_layout.addWidget(self.close_button) + + layout.addLayout(buttons_layout) + + self.setLayout(layout) + + def load_sales_history(self): + """Загрузка истории продаж партнера""" + try: + response = requests.get( + f"http://localhost:8000/api/v1/sales/partner/{self.partner_data['partner_id']}" + ) + if response.status_code == 200: + sales_data = response.json() + self.display_sales_data(sales_data) + else: + QMessageBox.warning(self, "Ошибка", "Не удалось загрузить историю продаж") + + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}") + + def display_sales_data(self, sales_data): + """Отображение данных о продажах в таблице""" + self.sales_table.setRowCount(len(sales_data)) + + total_quantity = 0 + for row, sale in enumerate(sales_data): + self.sales_table.setItem(row, 0, QTableWidgetItem(str(sale['sale_id']))) + self.sales_table.setItem(row, 1, QTableWidgetItem(sale['product_name'])) + self.sales_table.setItem(row, 2, QTableWidgetItem(str(sale['quantity']))) + self.sales_table.setItem(row, 3, QTableWidgetItem(sale['sale_date'])) + + total_quantity += float(sale['quantity']) + + # Обновление статистики + self.stats_label.setText( + f"Общее количество проданной продукции: {total_quantity}\n" + f"Всего продаж: {len(sales_data)}" + ) diff --git a/ressult/requirements.txt b/ressult/requirements.txt new file mode 100644 index 0000000..ace2609 --- /dev/null +++ b/ressult/requirements.txt @@ -0,0 +1,11 @@ +# requirements.txt +fastapi==0.104.1 +uvicorn==0.24.0 +psycopg2-binary==2.9.9 +python-dotenv==1.0.0 +python-multipart==0.0.6 +pandas==2.1.3 +openpyxl==3.1.2 +aiofiles==23.2.1 +pydantic[email]==2.5.0 +bcrypt==4.1.1 diff --git a/ressult/run.py b/ressult/run.py new file mode 100644 index 0000000..8bddcfa --- /dev/null +++ b/ressult/run.py @@ -0,0 +1,17 @@ +# run.py +""" +Точка входа для запуска сервера +""" +import uvicorn +import os +from dotenv import load_dotenv + +load_dotenv() + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host=os.getenv('HOST', '0.0.0.0'), + port=int(os.getenv('PORT', 8000)), + reload=os.getenv('DEBUG', 'False').lower() == 'true' + ) diff --git a/ressult/run_gui.py b/ressult/run_gui.py new file mode 100644 index 0000000..aacd95d --- /dev/null +++ b/ressult/run_gui.py @@ -0,0 +1,51 @@ +# run_gui.py +""" +Главный модуль запуска GUI приложения с авторизацией +""" +import sys +import os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from gui.login_window import LoginWindow +from gui.main_window import MainWindow +from PyQt6.QtWidgets import QApplication +from PyQt6.QtCore import QTimer + +class ApplicationController: + """Контроллер приложения, управляющий авторизацией и главным окном""" + + def __init__(self): + self.app = QApplication(sys.argv) + self.login_window = None + self.main_window = None + self.current_user = None + + def show_login(self): + """Показать окно авторизации""" + self.login_window = LoginWindow() + self.login_window.login_success.connect(self.on_login_success) + self.login_window.show() + + def on_login_success(self, user_data): + """Обработка успешной авторизации""" + self.current_user = user_data + self.login_window.close() + self.show_main_window() + + def show_main_window(self): + """Показать главное окно приложения""" + self.main_window = MainWindow(self.current_user) + self.main_window.show() + + def run(self): + """Запуск приложения""" + self.show_login() + return self.app.exec() + +def main(): + """Точка входа приложения""" + controller = ApplicationController() + sys.exit(controller.run()) + +if __name__ == "__main__": + main() diff --git a/robbery/master_pol-module_1_2/.gitignore b/robbery/master_pol-module_1_2/.gitignore new file mode 100644 index 0000000..eb7063d --- /dev/null +++ b/robbery/master_pol-module_1_2/.gitignore @@ -0,0 +1,2 @@ +**/__pycache__/ +.venv/ \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/.gitignore b/robbery/master_pol-module_1_2/.idea/.gitignore new file mode 100644 index 0000000..eaf91e2 --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/robbery/master_pol-module_1_2/.idea/.name b/robbery/master_pol-module_1_2/.idea/.name new file mode 100644 index 0000000..11a5d8e --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/.name @@ -0,0 +1 @@ +main.py \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml b/robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml b/robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml new file mode 100644 index 0000000..9bd607d --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/misc.xml b/robbery/master_pol-module_1_2/.idea/misc.xml new file mode 100644 index 0000000..953f9db --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/.idea/modules.xml b/robbery/master_pol-module_1_2/.idea/modules.xml new file mode 100644 index 0000000..f8a1763 --- /dev/null +++ b/robbery/master_pol-module_1_2/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/README.md b/robbery/master_pol-module_1_2/README.md new file mode 100644 index 0000000..1c67087 --- /dev/null +++ b/robbery/master_pol-module_1_2/README.md @@ -0,0 +1,55 @@ +# MasterPol + +Графическое приложение на PyQt6 для работы с базой данных MySQL. + +## Подготовка проекта + +1. **Клонируйте репозиторий и перейдите в папку проекта:** + + ```sh + git clone <адрес-репозитория> + cd master_pol + ``` + +2. **Создайте и активируйте виртуальное окружение:** + + ```sh + python -m venv .venv + .venv\Scripts\activate # Windows + # source .venv/bin/activate # Linux/MacOS + ``` + +3. **Установите зависимости:** + + ```sh + pip install -r requirements.txt + ``` + +4. **Создайте базу данных и выполните SQL-скрипт:** + + - Запустите MySQL и выполните скрипт `app/database/script.sql` для создания необходимых таблиц и данных: + + ```sh + mysql -u -p < app/database/script.sql + ``` + + - Замените `` и `` на свои значения. + +5. **Проверьте параметры подключения к базе данных:** + - Откройте файл `app/database/db.py` и убедитесь, что значения для подключения (host, user, password, database) указаны верно. + +## Запуск приложения + +```sh +python app/main.py +``` + +## Структура проекта + +- `app/main.py` — точка входа, запуск приложения +- `app/components/` — компоненты интерфейса +- `app/database/` — работа с БД, скрипты и настройки +- `app/pages/` — страницы приложения +- `app/res/` — ресурсы (цвета, шрифты) + +--- diff --git a/robbery/master_pol-module_1_2/app/components/edit_partner_dialog.py b/robbery/master_pol-module_1_2/app/components/edit_partner_dialog.py new file mode 100644 index 0000000..f596812 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/components/edit_partner_dialog.py @@ -0,0 +1,108 @@ +from PyQt6.QtWidgets import ( + QDialog, + QVBoxLayout, + QFormLayout, + QLineEdit, + QPushButton, + QComboBox, + QSpinBox, + QMessageBox, +) +from PyQt6.QtCore import Qt +from res.colors import ACCENT_COLOR +from dto.partners_dto import PartnerUpdateDto, PartnersInfo + + +class EditPartnerDialog(QDialog): + def __init__(self, partner_data: PartnersInfo, parent=None): + super().__init__(parent) + self.partner_data = partner_data + self.setup_ui() + self.load_partner_types() + self.fill_form() + self.result = None + + def setup_ui(self): + self.setWindowTitle("Редактирование партнера") + self.setFixedSize(500, 400) + + layout = QVBoxLayout() + form_layout = QFormLayout() + + # Создаем поля формы + self.partner_type = QComboBox() + self.partner_name = QLineEdit() + self.first_name = QLineEdit() + self.last_name = QLineEdit() + self.middle_name = QLineEdit() + self.email = QLineEdit() + self.phone = QLineEdit() + self.address = QLineEdit() + self.inn = QLineEdit() + self.rating = QSpinBox() + self.rating.setRange(0, 10) + + # Добавляем поля в форму + form_layout.addRow("Тип партнера:", self.partner_type) + form_layout.addRow("Название:", self.partner_name) + form_layout.addRow("Имя директора:", self.first_name) + form_layout.addRow("Фамилия директора:", self.last_name) + form_layout.addRow("Отчество директора:", self.middle_name) + form_layout.addRow("Email:", self.email) + form_layout.addRow("Телефон:", self.phone) + form_layout.addRow("Адрес:", self.address) + form_layout.addRow("ИНН:", self.inn) + form_layout.addRow("Рейтинг:", self.rating) + + # Кнопки + self.save_button = QPushButton("Сохранить") + self.cancel_button = QPushButton("Отмена") + + self.save_button.clicked.connect(self.save_changes) + self.cancel_button.clicked.connect(self.reject) + + layout.addLayout(form_layout) + layout.addWidget(self.save_button) + layout.addWidget(self.cancel_button) + + self.setLayout(layout) + + # Стили + self.setStyleSheet( + f""" + QPushButton {{ + background-color: {ACCENT_COLOR}; + padding: 8px; + border-radius: 4px; + }} + """ + ) + + def load_partner_types(self): + types = ['ООО', "ЗАО"] + for i, val in enumerate(types): + self.partner_type.addItem(val, i + 1) + + def fill_form(self): + pass + def save_changes(self): + try: + partner_data = PartnerUpdateDto( + id=self.partner_data.id, + partner_type_id=self.partner_type.currentData(), + partner_name=self.partner_name.text(), + first_name=self.first_name.text(), + last_name=self.last_name.text(), + middle_name=self.middle_name.text(), + email=self.email.text(), + phone=self.phone.text(), + address=self.address.text(), + inn=self.inn.text(), + rating=self.rating.value(), + ) + db.update_partner(partner_data) + self.accept() + except Exception as e: + QMessageBox.critical( + self, "Ошибка", f"Не удалось сохранить изменения: {str(e)}" + ) diff --git a/robbery/master_pol-module_1_2/app/components/partner_card.py b/robbery/master_pol-module_1_2/app/components/partner_card.py new file mode 100644 index 0000000..8b462a3 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/components/partner_card.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass +from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QFrame +from PyQt6.QtCore import Qt, pyqtSignal +from res.colors import ACCENT_COLOR, SECONDARY_COLOR +from res.fonts import MAIN_FONT +from dto.partners_dto import PartnersInfo + + + + +class PartnerCard(QFrame): + doubleClicked = pyqtSignal(PartnersInfo) + + def __init__(self, info: PartnersInfo): + super().__init__() + self.info = info + + self.init_ui() + self.set_styles() + + def mouseDoubleClickEvent(self, a0): + self.doubleClicked.emit(self.info) + return super().mouseDoubleClickEvent(a0) + + def init_ui(self): + main_layout = QVBoxLayout() + self.setLayout(main_layout) + + # Верхняя строка: Тип | Наименование и скидка + header_layout = QHBoxLayout() + header_text = QLabel(f"{self.info.type_name} | {self.info.partner_name}") + header_text.setObjectName("partnerHeader") + discount_text = QLabel(f"{self.info.discount}%") + discount_text.setObjectName("partnerDiscount") + + header_layout.addWidget(header_text) + header_layout.addWidget(discount_text, alignment=Qt.AlignmentFlag.AlignRight) + + # Информация о директоре + director_text = QLabel(f"Директор") + director_text.setObjectName("fieldLabel") + director_name = QLabel( + f"{self.info.last_name_director} {self.info.first_name_director} {self.info.middle_name_director}" + ) + + # Контактная информация + phone_text = QLabel(f"+{self.info.phone_partner}") + + # Рейтинг + rating_layout = QHBoxLayout() + rating_label = QLabel("Рейтинг:") + rating_label.setObjectName("fieldLabel") + rating_value = QLabel(str(self.info.rating)) + rating_layout.addWidget(rating_label) + rating_layout.addWidget(rating_value) + rating_layout.addStretch() + + # Добавляем все элементы в главный layout + main_layout.addLayout(header_layout) + main_layout.addWidget(director_text) + main_layout.addWidget(director_name) + main_layout.addWidget(phone_text) + main_layout.addLayout(rating_layout) + + def set_styles(self): + self.setStyleSheet( + """ + PartnerCard { + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px; + margin: 5px; + background-color: white; + } + QLabel { + font-family: %s; + } + #partnerHeader { + font-size: 18px; + font-weight: bold; + color: %s; + } + #partnerDiscount { + font-size: 18px; + font-weight: bold; + color: %s; + } + #fieldLabel { + color: gray; + font-size: 14px; + } + """ + % (MAIN_FONT, ACCENT_COLOR, SECONDARY_COLOR) + ) diff --git a/robbery/master_pol-module_1_2/app/database/db.py b/robbery/master_pol-module_1_2/app/database/db.py new file mode 100644 index 0000000..54590f0 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/database/db.py @@ -0,0 +1,84 @@ +import pymysql as psql +from dto.partners_dto import PartnerUpdateDto + + +class Database: + def __init__(self, host, user, password, db): + self.connection = psql.connect( + host=host, + user=user, + password=password, + database=db, + cursorclass=psql.cursors.DictCursor, + ) + + def authorize_user(self, username, password): + query = "SELECT * FROM users WHERE username=%s AND password=%s" + with self.connection.cursor() as cur: + cur.execute(query, (username, password)) + result = cur.fetchone() + return result is not None + + def execute_select(self, query, params=None): + """Выполняет SELECT запрос и возвращает результаты""" + with self.connection.cursor() as cur: + if params: + cur.execute(query, params) + else: + cur.execute(query) + return cur.fetchall() + + def get_partner_types(self): + """Получает все типы партнеров из таблицы partner_types""" + query = "SELECT * FROM partners_type" + with self.connection.cursor() as cur: + cur.execute(query) + return cur.fetchall() + + def update_partner(self, partners_info: PartnerUpdateDto): + with self.connection.cursor() as cur: + cur.callproc( + "upd_partner", + ( + partners_info.partner_type_id, + partners_info.id, + partners_info.partner_name, + partners_info.first_name, + partners_info.last_name, + partners_info.middle_name, + partners_info.email, + partners_info.phone, + partners_info.address, + partners_info.inn, + partners_info.rating, + ), + ) + self.connection.commit() + + def get_disc(self, partner_name): + """ + Получает скидку для партнера, вызывая функцию get_disc из БД + """ + # Сначала получим ID партнера по его имени + query = "SELECT id FROM partners WHERE partner_name = %s" + with self.connection.cursor() as cur: + cur.execute(query, (partner_name,)) + result = cur.fetchone() + + if not result: + return 0 + + # Вызываем функцию get_disc из БД + query = "SELECT get_disc(%s) as discount" + cur.execute(query, (result["id"],)) + discount_result = cur.fetchone() + + return discount_result["discount"] if discount_result else 0 + + +db = None +try: + db = Database(host="localhost", user="root", password="", db="master_pol") + print("Database connection established.") +except psql.MySQLError as e: + print(f"Error connecting to database: {e}") diff --git a/robbery/master_pol-module_1_2/app/database/script.sql b/robbery/master_pol-module_1_2/app/database/script.sql new file mode 100644 index 0000000..7d1b571 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/database/script.sql @@ -0,0 +1,460 @@ +CREATE DATABASE master_pol; +use master_pol; + +CREATE TABLE `partners` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `partner_type_id` INTEGER NOT NULL, + `partner_name` VARCHAR(255) NOT NULL, + `first_name_director` VARCHAR(50) NOT NULL, + `last_name_director` VARCHAR(50) NOT NULL, + `middle_name_director` VARCHAR(255), + `email_partner` VARCHAR(100) NOT NULL, + `phone_partner` VARCHAR(15) NOT NULL, + `address` VARCHAR(255) NOT NULL, + `INN` VARCHAR(10) NOT NULL, + `rating` INTEGER NOT NULL, + `logo` LONGBLOB, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `partners_type` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(255), + PRIMARY KEY(`id`) +); + + +CREATE TABLE `products` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `article` VARCHAR(10) NOT NULL, + `name` VARCHAR(100) NOT NULL, + `product_type_id` INTEGER NOT NULL, + `description` VARCHAR(255), + `picture` LONGBLOB, + `min_price_partners` DECIMAL(10,2) NOT NULL, + `cert_quality` LONGBLOB, + `standard_number` VARCHAR(255), + `selfcost` DECIMAL(10,2), + `length` DECIMAL(10,2), + `width` DECIMAL(10,2), + `height` DECIMAL(10,2), + `weight_no_package` DECIMAL(10,2), + `weight_with_package` DECIMAL(10,2), + `time_to_create_min` INTEGER, + `workshop_number` INTEGER, + `people_count_production` INTEGER, + `product_current_stock` INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `products_types` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(70) NOT NULL, + `coefficent` DECIMAL(3,2) NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `product_partners` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `product_id` INTEGER NOT NULL, + `partner_id` INTEGER NOT NULL, + `amount` INTEGER NOT NULL, + `sale_date` DATE NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `employees` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `employee_type_id` INTEGER NOT NULL, + `first_name` VARCHAR(50) NOT NULL, + `last_name` VARCHAR(50) NOT NULL, + `middle_name` VARCHAR(60) NULL, + `birth_date` DATE NOT NULL, + `passport_data` VARCHAR(11) NOT NULL, + `bank_details` VARCHAR(100) NOT NULL, + `has_family` BOOLEAN, + `health_status` VARCHAR(25), + PRIMARY KEY(`id`) +); + + +CREATE TABLE `employees_types` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(50) NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `users` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `username` VARCHAR(30) NOT NULL, + `password` VARCHAR(80) NOT NULL, + `employee_id` INTEGER NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `materials` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `material_type_id` INTEGER NOT NULL, + `supplier_id` INTEGER NOT NULL, + `name` VARCHAR(60) NOT NULL, + `package_quantity` INTEGER NOT NULL, + `unit` VARCHAR(20) NOT NULL, + `cost` DECIMAL(8,2) NOT NULL, + `image` LONGBLOB, + `min_stock` INTEGER, + `material_current_stock` INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `materials_type` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(50) NOT NULL, + `defect_percent` DECIMAL(10,2) NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `products_recipes` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `product_id` INTEGER NOT NULL, + `material_id` INTEGER NOT NULL, + `material_count` INTEGER NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `partners_rating_history` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `partner_id` INTEGER NOT NULL, + `new_rating` INTEGER NOT NULL, + `changed` DATETIME NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `orders` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `partner_id` INTEGER NOT NULL, + `manager_id` INTEGER NOT NULL, + `total_price` DECIMAL(10,2) NOT NULL, + `order_payment` DECIMAL(10,2) NOT NULL DEFAULT 0, + `created` DATETIME NOT NULL, + `status` ENUM('created', 'waiting prepayment', 'prepayment received', 'completed', 'canceled', 'ready for shipment', 'pending', 'in production') NOT NULL, + `prepayment_date` DATETIME, + `payment_date` DATETIME, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `products_orders` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `order_id` INTEGER NOT NULL, + `product_id` INTEGER NOT NULL, + `quantity` INTEGER NOT NULL, + `agreed_price_per` DECIMAL(8,2), + `production_date` DATE, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `suppliers` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `name` VARCHAR(50) NOT NULL, + `INN` VARCHAR(10) NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `materials_supply_history` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `material_id` INTEGER NOT NULL, + `supplier_id` INTEGER NOT NULL, + `quantity` INTEGER NOT NULL, + `delivery_date` DATE NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `materials_movement` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `material_id` INTEGER NOT NULL, + `amount` INTEGER NOT NULL, + `movement_type` ENUM('incoming', 'reserve', 'write off') NOT NULL DEFAULT 'incoming', + `movement_date` DATETIME NOT NULL, + PRIMARY KEY(`id`) +); + + +CREATE TABLE `employees_access` ( + `id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE, + `employee_id` INTEGER NOT NULL, + `door_id` INTEGER NOT NULL, + `access_date` DATETIME NOT NULL, + PRIMARY KEY(`id`) +); + + +ALTER TABLE `partners` +ADD FOREIGN KEY(`partner_type_id`) REFERENCES `partners_type`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products` +ADD FOREIGN KEY(`product_type_id`) REFERENCES `products_types`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `product_partners` +ADD FOREIGN KEY(`product_id`) REFERENCES `products`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `product_partners` +ADD FOREIGN KEY(`partner_id`) REFERENCES `partners`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `employees` +ADD FOREIGN KEY(`employee_type_id`) REFERENCES `employees_types`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `users` +ADD FOREIGN KEY(`employee_id`) REFERENCES `employees`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials` +ADD FOREIGN KEY(`material_type_id`) REFERENCES `materials_type`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products_recipes` +ADD FOREIGN KEY(`product_id`) REFERENCES `products`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products_recipes` +ADD FOREIGN KEY(`material_id`) REFERENCES `materials`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `partners_rating_history` +ADD FOREIGN KEY(`partner_id`) REFERENCES `partners`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `orders` +ADD FOREIGN KEY(`partner_id`) REFERENCES `partners`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `orders` +ADD FOREIGN KEY(`manager_id`) REFERENCES `employees`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products_orders` +ADD FOREIGN KEY(`order_id`) REFERENCES `orders`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `products_orders` +ADD FOREIGN KEY(`product_id`) REFERENCES `products`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials` +ADD FOREIGN KEY(`supplier_id`) REFERENCES `suppliers`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials_supply_history` +ADD FOREIGN KEY(`material_id`) REFERENCES `materials`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials_supply_history` +ADD FOREIGN KEY(`supplier_id`) REFERENCES `suppliers`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `materials_movement` +ADD FOREIGN KEY(`material_id`) REFERENCES `materials`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; +ALTER TABLE `employees_access` +ADD FOREIGN KEY(`employee_id`) REFERENCES `employees`(`id`) +ON UPDATE NO ACTION ON DELETE NO ACTION; + +INSERT INTO materials_type (name, defect_percent) VALUES +('Тип материала 1', 0.001), +('Тип материала 2', 0.0095), +('Тип материала 3', 0.0028), +('Тип материала 4', 0.0055), +('Тип материала 5', 0.0034); + +INSERT INTO products_types (name, coefficent) VALUES +('Ламинат', 2.35), +('Массивная доска', 5.15), +('Паркетная доска', 4.34), +('Пробковое покрытие', 1.5); + +INSERT INTO partners_type (name) VALUES +('ЗАО'), +('ООО'), +('ПАО'), +('ОАО'); + + +INSERT INTO partners (partner_type_id, partner_name, first_name_director, last_name_director, middle_name_director, email_partner, phone_partner, address, INN, rating) VALUES +(1, 'База Строитель', 'Александра', 'Иванова', 'Ивановна', 'aleksandraivanova@ml.ru', '4931234567', '652050, Кемеровская область, город Юрга, ул. Лесная, 15', '2222455179', 7), +(2, 'Паркет 29', 'Василий', 'Петров', 'Петрович', 'vppetrov@vl.ru', '9871235678', '164500, Архангельская область, город Северодвинск, ул. Строителей, 18', '3333888520', 7), +(3, 'Стройсервис', 'Андрей', 'Соловьев', 'Николаевич', 'ansolovev@st.ru', '8122233200', '188910, Ленинградская область, город Приморск, ул. Парковая, 21', '4440391035', 7), +(4, 'Ремонт и отделка', 'Екатерина', 'Воробьева', 'Валерьевна', 'ekaterina.vorobeva@ml.ru', '4442223311', '143960, Московская область, город Реутов, ул. Свободы, 51', '1111520857', 5), +(1, 'МонтажПро', 'Степан', 'Степанов', 'Сергеевич', 'stepanov@stepan.ru', '9128883333', '309500, Белгородская область, город Старый Оскол, ул. Рабочая, 122', '5552431140', 10); + +INSERT INTO products (article, name, product_type_id, min_price_partners) VALUES +('8758385', 'Паркетная доска Ясень темный однополосная 14 мм', 3, 4456.90), +('8858958', 'Инженерная доска Дуб Французская елка однополосная 12 мм', 3, 7330.99), +('7750282', 'Ламинат Дуб дымчато-белый 33 класс 12 мм', 1, 1799.33), +('7028748', 'Ламинат Дуб серый 32 класс 8 мм с фаской', 1, 3890.41), +('5012543', 'Пробковое напольное клеевое покрытие 32 класс 4 мм', 4, 5450.59); + +INSERT INTO product_partners (product_id, partner_id, amount, sale_date) VALUES +(1, 1, 15500, '2023-03-23'), +(3, 1, 12350, '2023-12-18'), +(4, 1, 37400, '2024-06-07'), +(2, 2, 35000, '2022-12-02'), +(5, 2, 1250, '2023-05-17'), +(3, 2, 1000, '2024-06-07'), +(1, 2, 7550, '2024-07-01'), +(1, 3, 7250, '2023-01-22'), +(2, 3, 2500, '2024-07-05'), +(4, 4, 59050, '2023-03-20'), +(3, 4, 37200, '2024-03-12'), +(5, 4, 4500, '2024-05-14'), +(3, 5, 50000, '2023-09-19'), +(4, 5, 670000, '2023-11-10'), +(1, 5, 35000, '2024-04-15'), +(2, 5, 25000, '2024-06-12'); + +-- === 1. Типы сотрудников === +INSERT INTO employees_types (name) +VALUES +('Менеджер'), +('Бухгалтер'), +('Программист'), +('Охранник'), +('Уборщик'); + +-- === 2. Сотрудники === +INSERT INTO employees ( + employee_type_id, first_name, last_name, middle_name, birth_date, + passport_data, bank_details, has_family, health_status +) +VALUES +-- Менеджеры +(1, 'Иван', 'Петров', 'Сергеевич', '1988-03-15', '40051234567', '123456789', TRUE, 'Хорошее'), +(1, 'Мария', 'Сидорова', 'Игоревна', '1990-11-02', '40057891234', '987654321', FALSE, 'Отличное'), + +-- Программист +(3, 'Андрей', 'Кузнецов', 'Алексеевич', '1995-07-21', '40101234567', '111122223333', TRUE, 'Хорошее'), + +-- Бухгалтер +(2, 'Елена', 'Морозова', 'Павловна', '1982-05-08', '40104561234', '444455556666', TRUE, 'Удовлетворительное'), + +-- Охранник +(4, 'Сергей', 'Волков', 'Владимирович', '1979-09-10', '40205678901', '555566667777', FALSE, 'Хорошее'), + +-- Уборщик +(5, 'Наталья', 'Орлова', 'Геннадьевна', '1975-12-25', '40307891234', '888899990000', TRUE, 'Хорошее'); + +-- === 3. Пользователи === +-- Пользователи, связанные с менеджерами +INSERT INTO users (username, password, employee_id) +VALUES +('ivan', 'test', 1), +('manager_maria', 'hashed_password_456', 2); + + +CREATE VIEW show_partners +AS +SELECT p.id, pt.name AS type_name, p.partner_name, p.first_name_director, p.last_name_director, p.middle_name_director, p.phone_partner, p.rating +FROM partners p JOIN partners_type pt +ON +p.partner_type_id = pt.id; + + +DELIMITER // +CREATE PROCEDURE add_parther (IN p_partner_type_id INT, IN p_partner_name VARCHAR(255), +IN p_first_name_director VARCHAR(50), IN p_last_name_director VARCHAR(50), IN p_middle_name_director VARCHAR(255), +IN p_email_partner VARCHAR(100), IN p_phone_partner VARCHAR(15), IN p_address VARCHAR(255), IN p_INN VARCHAR(10), IN p_rating INT) + +BEGIN + INSERT INTO partners ( + partner_type_id, + partner_name, + first_name_director, + last_name_director, + middle_name_director, + email_partner, + phone_partner, + address, + INN, + rating + ) VALUES ( + p_partner_type_id, + p_partner_name, + p_first_name_director, + p_last_name_director, + p_middle_name_director, + p_email_partner, + p_phone_partner, + p_address, + p_INN, + p_rating + ); +END // + +DELIMITER ; + + +DELIMITER // + +CREATE PROCEDURE upd_partner (IN p_partner_type_id INT, IN p_id INT, IN p_partner_name VARCHAR(255), +IN p_first_name_director VARCHAR(50), IN p_last_name_director VARCHAR(50), IN p_middle_name_director VARCHAR(255), +IN p_email_partner VARCHAR(100), IN p_phone_partner VARCHAR(15), IN p_address VARCHAR(255), IN p_INN VARCHAR(10), IN p_rating INT) + +BEGIN + UPDATE partners + SET + partner_type_id = p_partner_type_id, + partner_name = p_partner_name, + first_name_director = p_first_name_director, + last_name_director = p_last_name_director, + middle_name_director = p_middle_name_director, + email_partner = p_email_partner, + phone_partner = p_phone_partner, + address = p_address, + INN = p_INN, + rating = p_rating + WHERE id = p_id; + +END // + +DELIMITER ; + + +DELIMITER // + +CREATE FUNCTION get_disc(partner_id INT) +RETURNS INT +BEGIN + + DECLARE total_amount INT; + + SELECT SUM(amount) INTO total_amount + FROM product_partners + WHERE partner_id = partner_id; + + IF total_amount >= 300000 THEN RETURN 15; + ELSEIF total_amount >= 50000 THEN RETURN 10; + ELSEIF total_amount >= 10000 THEN RETURN 5; + ELSE RETURN 0; + END IF; + +END // + +DELIMITER ; + + + +DELIMITER // + +CREATE PROCEDURE partner_history(IN p_partner_id INT) +BEGIN + SELECT + pr.name AS product_name, + pp.amount AS quantity, + pp.sale_date AS sale_date + FROM product_partners pp JOIN products pr + ON + pp.product_id = pr.id + WHERE pp.partner_id = p_partner_id + ORDER BY pp.sale_date DESC; +END// + +DELIMITER ; diff --git a/robbery/master_pol-module_1_2/app/dto/partners_dto.py b/robbery/master_pol-module_1_2/app/dto/partners_dto.py new file mode 100644 index 0000000..63b1107 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/dto/partners_dto.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass + + +@dataclass +class PartnersInfo: + id: int + type_name: str + partner_name: str + first_name_director: str + last_name_director: str + middle_name_director: str + phone_partner: str + rating: int + discount: float + + +@dataclass +class PartnerUpdateDto: + id: int + partner_type_id: int + partner_name: str + first_name: str + last_name: str + middle_name: str + email: str + phone: str + address: str + inn: str + rating: int diff --git a/robbery/master_pol-module_1_2/app/main.py b/robbery/master_pol-module_1_2/app/main.py new file mode 100644 index 0000000..2f48f74 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/main.py @@ -0,0 +1,11 @@ +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QIcon +from pages.auth_page import AuthPage + +app = QApplication([]) + +app.setWindowIcon(QIcon("app/res/imgs/master_pol.ico")) +start_page = AuthPage() +start_page.show() + +app.exec() diff --git a/robbery/master_pol-module_1_2/app/pages/auth_page.py b/robbery/master_pol-module_1_2/app/pages/auth_page.py new file mode 100644 index 0000000..2881fa0 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/pages/auth_page.py @@ -0,0 +1,94 @@ +from PyQt6.QtWidgets import ( + QWidget, + QLabel, + QFormLayout, + QPushButton, + QMessageBox, + QLineEdit, + QVBoxLayout, +) +from PyQt6.QtCore import Qt +from res.colors import ACCENT_COLOR, SECONDARY_COLOR, ACCENT_COLOR_HOVER +from res.fonts import MAIN_FONT + + +class AuthPage(QWidget): + def __init__(self): + super().__init__() + self.setup_window() + self.init_ui() + self.set_styles() + + def setup_window(self): + self.setWindowTitle("Авторизация") + self.setFixedSize(400, 250) + + def init_ui(self): + self.main_layout = QVBoxLayout() + self.form_layout: QFormLayout = QFormLayout() + + self.title = QLabel("Авторизация") + self.title.setObjectName("title") + + self.username_label = QLabel("Логин:") + self.password_label = QLabel("Пароль:") + + self.username_input = QLineEdit() + self.password_input = QLineEdit() + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) + + self.login_button = QPushButton("Войти") + + self.form_layout.addRow(self.username_label, self.username_input) + self.form_layout.addRow(self.password_label, self.password_input) + self.form_layout.addRow(self.login_button) + + self.setLayout(self.main_layout) + self.main_layout.addWidget(self.title, alignment=Qt.AlignmentFlag.AlignHCenter) + + self.main_layout.addStretch() + self.main_layout.addLayout(self.form_layout) + self.main_layout.addStretch() + + self.login_button.clicked.connect(self.handle_login) + + def handle_login(self): + username = self.username_input.text() + password = self.password_input.text() + + if not username or not password: + QMessageBox.warning(self, "Ошибка", "Пожалуйста, заполните все поля.") + return + + from pages.partners_page import PartnersPage + + self.partners_page = PartnersPage() + self.partners_page.show() + self.close() + + def set_styles(self): + self.setStyleSheet( + """QLabel { font-size: 16px; font-family: %(MAIN_FONT)s} + #title { + font-size: 24px; + font-weight: bold; + color: %(ACCENT_COLOR)s; + } + QPushButton { + background-color: %(ACCENT_COLOR)s; + border: 1px solid black; + color: %(SECONDARY_COLOR)s; + font-weight: bold; + padding: 5px; + } + QPushButton:hover { + background-color: %(ACCENT_COLOR_HOVER)s; + } + """ + % { + "ACCENT_COLOR": ACCENT_COLOR, + "SECONDARY_COLOR": SECONDARY_COLOR, + "MAIN_FONT": MAIN_FONT, + "ACCENT_COLOR_HOVER": ACCENT_COLOR_HOVER, + } + ) diff --git a/robbery/master_pol-module_1_2/app/pages/partners_page.py b/robbery/master_pol-module_1_2/app/pages/partners_page.py new file mode 100644 index 0000000..9b2e804 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/pages/partners_page.py @@ -0,0 +1,130 @@ +from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QScrollArea, QVBoxLayout +from PyQt6.QtCore import Qt +from components.partner_card import PartnerCard, PartnersInfo +from res.colors import ACCENT_COLOR + + +class PartnersPage(QWidget): + def __init__(self): + super().__init__() + self.setup_window() + self.init_ui() + self.load_partners() + + def setup_window(self): + self.setWindowTitle("Партнеры") + self.resize(800, 600) + + def init_ui(self): + main_layout = QVBoxLayout() + self.setLayout(main_layout) + + # Заголовок + title = QLabel("Партнеры") + title.setObjectName("title") + title.setStyleSheet( + f""" + #title {{ + font-size: 24px; + font-weight: bold; + color: {ACCENT_COLOR}; + margin-bottom: 20px; + }} + """ + ) + main_layout.addWidget(title, alignment=Qt.AlignmentFlag.AlignHCenter) + + # Создаем область прокрутки + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_content = QWidget() + self.partners_layout = QVBoxLayout(scroll_content) + scroll_area.setWidget(scroll_content) + main_layout.addWidget(scroll_area) + + def handle_partner_double_click(self, partner_info: PartnersInfo): + from components.edit_partner_dialog import EditPartnerDialog + + dialog = EditPartnerDialog(partner_info, self) + dialog.exec() + + def load_partners(self): + # Тестовые данные партнеров + test_partners = [ + { + "id": 1, + "type_name": "Золотой партнер", + "partner_name": "ООО 'ТехноПрофи'", + "first_name_director": "Иван", + "last_name_director": "Петров", + "middle_name_director": "Сергеевич", + "phone_partner": "+7 (495) 123-45-67", + "rating": 4.8, + "discount": 15.0 + }, + { + "id": 2, + "type_name": "Серебряный партнер", + "partner_name": "ИП Сидоров А.В.", + "first_name_director": "Алексей", + "last_name_director": "Сидоров", + "middle_name_director": "Викторович", + "phone_partner": "+7 (495) 234-56-78", + "rating": 4.2, + "discount": 10.0 + }, + { + "id": 3, + "type_name": "Бронзовый партнер", + "partner_name": "ООО 'СтройМастер'", + "first_name_director": "Мария", + "last_name_director": "Иванова", + "middle_name_director": "Олеговна", + "phone_partner": "+7 (495) 345-67-89", + "rating": 3.9, + "discount": 7.5 + }, + { + "id": 4, + "type_name": "Золотой партнер", + "partner_name": "АО 'ПромИнвест'", + "first_name_director": "Сергей", + "last_name_director": "Козлов", + "middle_name_director": "Анатольевич", + "phone_partner": "+7 (495) 456-78-90", + "rating": 4.9, + "discount": 18.0 + }, + { + "id": 5, + "type_name": "Стандартный партнер", + "partner_name": "ООО 'ТоргСервис'", + "first_name_director": "Ольга", + "last_name_director": "Смирнова", + "middle_name_director": "Дмитриевна", + "phone_partner": "+7 (495) 567-89-01", + "rating": 3.5, + "discount": 5.0 + } + ] + + # Создаем карточки партнеров на основе тестовых данных + for partner in test_partners: + partner_info = PartnersInfo( + id=partner["id"], + type_name=partner["type_name"], + partner_name=partner["partner_name"], + first_name_director=partner["first_name_director"], + last_name_director=partner["last_name_director"], + middle_name_director=partner["middle_name_director"], + phone_partner=partner["phone_partner"], + rating=partner["rating"], + discount=partner["discount"], + ) + + # Создаем и добавляем карточку партнера + partner_card = PartnerCard(partner_info) + partner_card.doubleClicked.connect(self.handle_partner_double_click) + self.partners_layout.addWidget(partner_card) + + self.partners_layout.addStretch() \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/app/res/colors.py b/robbery/master_pol-module_1_2/app/res/colors.py new file mode 100644 index 0000000..b4165d4 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/res/colors.py @@ -0,0 +1,4 @@ +MAIN_COLOR = "#FFFFFF" +SECONDARY_COLOR = "#F4E8D3" +ACCENT_COLOR = "#67BA80" +ACCENT_COLOR_HOVER = "#529265" diff --git a/robbery/master_pol-module_1_2/app/res/fonts.py b/robbery/master_pol-module_1_2/app/res/fonts.py new file mode 100644 index 0000000..207a164 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/res/fonts.py @@ -0,0 +1 @@ +MAIN_FONT = "Segoe UI" \ No newline at end of file diff --git a/robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico b/robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico new file mode 100644 index 0000000..9744b0a Binary files /dev/null and b/robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico differ diff --git a/robbery/master_pol-module_1_2/app/res/imgs/master_pol.png b/robbery/master_pol-module_1_2/app/res/imgs/master_pol.png new file mode 100644 index 0000000..c192a72 Binary files /dev/null and b/robbery/master_pol-module_1_2/app/res/imgs/master_pol.png differ diff --git a/robbery/master_pol-module_1_2/app/res/styles.py b/robbery/master_pol-module_1_2/app/res/styles.py new file mode 100644 index 0000000..e76ca02 --- /dev/null +++ b/robbery/master_pol-module_1_2/app/res/styles.py @@ -0,0 +1,35 @@ +from string import Template +from res.colors import MAIN_COLOR, SECONDARY_COLOR, ACCENT_COLOR +from res.fonts import MAIN_FONT + +styles_template = Template( + """ + QWidget { + font-family: {MAIN_FONT}; + background-color: {MAIN_COLOR} + color: {SECONDARY_COLOR}; + } + QPushButton { + background-color: {ACCENT_COLOR}; + border: none; + padding: 8px 16px; + border-radius: 4px; + } + QPushButton:hover { + background-color: {SECONDARY_COLOR}; + } + QLineEdit { + padding: 6px; + border: 1px solid {ACCENT_COLOR}; + border-radius: 4px; + background-color: white; + } +""" +) + +styles = styles_template.substitute( + MAIN_FONT=MAIN_FONT, + MAIN_COLOR=MAIN_COLOR, + SECONDARY_COLOR=SECONDARY_COLOR, + ACCENT_COLOR=ACCENT_COLOR, +) diff --git a/robbery/master_pol-module_1_2/requirements.txt b/robbery/master_pol-module_1_2/requirements.txt new file mode 100644 index 0000000..bbd2d4f Binary files /dev/null and b/robbery/master_pol-module_1_2/requirements.txt differ diff --git a/service_requests.db b/service_requests.db index 2594dc9..569cec3 100644 Binary files a/service_requests.db and b/service_requests.db differ diff --git a/service_requests_v2.db b/service_requests_v2.db index 977ae18..7c9edb6 100644 Binary files a/service_requests_v2.db and b/service_requests_v2.db differ