commit e4fb186ee96872a2ad20e2d5857c28c6d6787873 Author: helldh Date: Tue Nov 25 19:55:19 2025 +0300 Inital Commit diff --git a/main.py b/main.py new file mode 100644 index 0000000..354b92c --- /dev/null +++ b/main.py @@ -0,0 +1,1822 @@ +import sys +import sqlite3 +import os +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' +} + + +# === Инициализация базы данных 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 (full_name, position, hire_date, salary, is_active) + VALUES ('Иванов Алексей Петрович', 'Менеджер по продажам', '2023-01-15', 50000.00, 1) + """) + + # Добавляем партнеров + partners_data = [ + ("ООО", "ООО «СтройГрад»", "г. Москва, ул. Ленина, 10", "770123456789", "Иван Петров", "+79001112233", "buildgrad@example.com", 4.5, "Москва, СПб", 1500000.00, 5.0), + ("ИП", "ИП Сидоров А.В.", "г. Казань, пр. Победы, 5", "165432109876", "Андрей Сидоров", "+79054445566", "sidorov@example.com", 4.2, "Казань", 800000.00, 3.0), + ("ТОО", "Торговый дом «Полимер+»", "г. Екатеринбург, ул. Мира, 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_type, company_name, legal_address, inn, director_name, phone, email, rating, sales_locations, total_sales, discount_rate) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, p) + + # Добавляем поставщиков + suppliers_data = [ + ("ООО", "ООО «Сырье-Про»", "770987654321", "Петр Васильев", "+79012345678", "syrie@example.com", 4.7, "Древесина, клей, ламинация"), + ("ИП", "ИП Колесников С.И.", "163218765432", "Сергей Колесников", "+79087654321", "kolesnikov@example.com", 4.3, "ПВХ, пластификаторы"), + ] + + for s in suppliers_data: + cursor.execute(""" + INSERT OR IGNORE INTO suppliers + (supplier_type, company_name, inn, contact_person, phone, email, rating, supplied_materials) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, s) + + # Добавляем материалы + materials_data = [ + ("Древесина", "Дубовая доска", 1, 1.0, "м³", "Дубовая доска высшего сорта", 15000.00, 100.5, 20.0), + ("Клей", "Клей для ламината", 1, 25.0, "кг", "Водостойкий клей", 450.00, 500.0, 50.0), + ("ПВХ", "ПВХ пленка", 2, 50.0, "м²", "Декоративная ПВХ пленка", 320.00, 800.0, 100.0), + ] + + for m in materials_data: + cursor.execute(""" + INSERT OR IGNORE INTO materials + (material_type, material_name, supplier_id, package_quantity, unit_of_measure, description, cost_per_unit, current_stock, min_stock_level) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, m) + + # Добавляем продукцию + products_data = [ + ("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), + ("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), + ("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 + (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, 0.05), # Ламинат Quick-Step -> Дубовая доска + (1, 2, 0.8), # Ламинат Quick-Step -> Клей + (1, 3, 1.2), # Ламинат Quick-Step -> ПВХ пленка + (2, 1, 0.06), # Ламинат Classen -> Дубовая доска + (2, 2, 1.0), # Ламинат Classen -> Клей + (2, 3, 1.5), # Ламинат Classen -> ПВХ пленка + (3, 3, 0.3), # Плинтус -> ПВХ пленка + ] + + for pm in product_materials_data: + cursor.execute(""" + INSERT OR IGNORE INTO product_materials (product_id, material_id, material_quantity) + VALUES (?, ?, ?) + """, pm) + + # Добавляем запасы готовой продукции + finished_goods_data = [ + (1, 500.0, 0, 50.0), + (2, 300.0, 0, 30.0), + (3, 1000.0, 0, 100.0), + ] + + for fg in finished_goods_data: + cursor.execute(""" + INSERT OR IGNORE INTO finished_goods_stock (product_id, current_stock, reserved_stock, min_stock_level) + VALUES (?, ?, ?, ?) + """, fg) + + +# === Диалог авторизации === +class AuthDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Авторизация - Мастер пол") + self.setFixedSize(350, 200) + 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 {{ + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + }} + """) + + self.authenticated = False + self.user_id = None + self.user_name = 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.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") + hint.setAlignment(Qt.AlignmentFlag.AlignCenter) + hint.setStyleSheet("color: #666; font-size: 12px; margin-top: 10px;") + layout.addWidget(hint) + + self.setLayout(layout) + + def login(self): + login = self.login_edit.text().strip() + password = self.pass_edit.text() + + # Простая проверка (в реальной системе - проверка в БД) + if login == "manager" and password == "pass123": + self.authenticated = True + self.user_id = 1 # ID менеджера из тестовых данных + self.user_name = "Иванов Алексей Петрович" + 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): + super().__init__() + self.user_id = user_id + self.user_name = user_name + + self.setWindowTitle(f"Мастер пол - Система управления (Пользователь: {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"Пользователь: {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) + + 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.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.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.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("🔄 Обновить") + + 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("🔄 Обновить") + + 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("🔄 Обновить") + + 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}") + 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 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 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 + + # Здесь должна быть реализация загрузки и редактирования заявки + QMessageBox.information(self, "Информация", "Редактирование заявки будет реализовано в полной версии.") + + def view_order(self): + """Просмотр деталей заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + # Здесь должна быть реализация просмотра деталей заявки + QMessageBox.information(self, "Информация", "Просмотр заявки будет реализовано в полной версии.") + + def update_order_status(self): + """Обновление статуса заявки""" + order_id = self.get_selected_order_id() + if not order_id: + return + + # Здесь должна быть реализация обновления статуса + QMessageBox.information(self, "Информация", "Обновление статуса будет реализовано в полной версии.") + + 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() + + +# === Точка входа === +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) + main_window.show() + + sys.exit(app.exec()) diff --git a/masterpol.db b/masterpol.db new file mode 100644 index 0000000..a696f89 Binary files /dev/null and b/masterpol.db differ