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