diff --git a/Catalog_import.xlsx b/Catalog_import.xlsx new file mode 100644 index 0000000..5b09b94 Binary files /dev/null and b/Catalog_import.xlsx differ diff --git a/Categories_import.xlsx b/Categories_import.xlsx new file mode 100644 index 0000000..9a327dc Binary files /dev/null and b/Categories_import.xlsx differ diff --git a/Clients_import.xlsx b/Clients_import.xlsx new file mode 100644 index 0000000..9361b85 Binary files /dev/null and b/Clients_import.xlsx differ diff --git a/Materials_import.xlsx b/Materials_import.xlsx new file mode 100644 index 0000000..0c40373 Binary files /dev/null and b/Materials_import.xlsx differ diff --git a/OrderItems.xlsx b/OrderItems.xlsx new file mode 100644 index 0000000..34a0a4d Binary files /dev/null and b/OrderItems.xlsx differ diff --git a/Orders_import.xlsx b/Orders_import.xlsx new file mode 100644 index 0000000..4c54efc Binary files /dev/null and b/Orders_import.xlsx differ diff --git a/Products_import.xlsx b/Products_import.xlsx new file mode 100644 index 0000000..3ea8c76 Binary files /dev/null and b/Products_import.xlsx differ diff --git a/Resource/Catalog_import.xlsx b/Resource/Catalog_import.xlsx new file mode 100644 index 0000000..5b09b94 Binary files /dev/null and b/Resource/Catalog_import.xlsx differ diff --git a/Resource/Categories_import.xlsx b/Resource/Categories_import.xlsx new file mode 100644 index 0000000..9a327dc Binary files /dev/null and b/Resource/Categories_import.xlsx differ diff --git a/Resource/Clients_import.xlsx b/Resource/Clients_import.xlsx new file mode 100644 index 0000000..9361b85 Binary files /dev/null and b/Resource/Clients_import.xlsx differ diff --git a/Resource/Materials_import.xlsx b/Resource/Materials_import.xlsx new file mode 100644 index 0000000..0c40373 Binary files /dev/null and b/Resource/Materials_import.xlsx differ diff --git a/Resource/OrderItems.xlsx b/Resource/OrderItems.xlsx new file mode 100644 index 0000000..34a0a4d Binary files /dev/null and b/Resource/OrderItems.xlsx differ diff --git a/Resource/Orders_import.xlsx b/Resource/Orders_import.xlsx new file mode 100644 index 0000000..4c54efc Binary files /dev/null and b/Resource/Orders_import.xlsx differ diff --git a/Resource/Products_import.xlsx b/Resource/Products_import.xlsx new file mode 100644 index 0000000..3ea8c76 Binary files /dev/null and b/Resource/Products_import.xlsx differ diff --git a/Resource/favicon.png b/Resource/favicon.png new file mode 100644 index 0000000..77a345d Binary files /dev/null and b/Resource/favicon.png differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..8d3b487 --- /dev/null +++ b/main.py @@ -0,0 +1,588 @@ +import openpyxl # Добавь это +openpyxl.__version__ = "3.1.5" # И это +import sqlite3 +import pandas as pd +import os +import sys +from datetime import datetime + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +RESOURCE_DIR = os.path.join(BASE_DIR, "Resource") +# ========== ИЗМЕНЕНО: partners.db -> materials.db ========== +DB_PATH = os.path.join(BASE_DIR, "materials.db") + + +def create_tables(db_connection): + """Создает все необходимые таблицы в базе данных""" + cursor = db_connection.cursor() + + cursor.executescript(''' + -- Типы контрагентов + CREATE TABLE IF NOT EXISTS ContractorType ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type_name VARCHAR(100) NOT NULL UNIQUE + ); + + -- Контрагенты/Клиенты + CREATE TABLE IF NOT EXISTS Clients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + contractor_type_id INTEGER REFERENCES ContractorType(id), + name VARCHAR(255) NOT NULL, + director VARCHAR(255), + email VARCHAR(255), + phone VARCHAR(50), + address TEXT, + inn VARCHAR(20) UNIQUE, + rating DECIMAL(3,2) DEFAULT 0.0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Категории продукции + CREATE TABLE IF NOT EXISTS Categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_name VARCHAR(100) NOT NULL UNIQUE, + coefficient DECIMAL(5,2) NOT NULL DEFAULT 1.0 + ); + + -- Таблица материалов + CREATE TABLE IF NOT EXISTS Materials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + material_name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + category VARCHAR(50), + color VARCHAR(50), + density DECIMAL(10,2), + thickness DECIMAL(10,2), + supplier VARCHAR(255), + country VARCHAR(100), + wear_class VARCHAR(20), + price_per_unit DECIMAL(15,2), + unit VARCHAR(20) DEFAULT 'шт', + in_stock INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Каталог продукции + CREATE TABLE IF NOT EXISTS Catalog ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER REFERENCES Categories(id), + material_id INTEGER REFERENCES Materials(id), + product_name VARCHAR(255) NOT NULL, + article VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + min_price DECIMAL(15,2) NOT NULL, + quantity_in_stock INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Таблица корзин + CREATE TABLE IF NOT EXISTS Cart ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER REFERENCES Clients(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status VARCHAR(50) DEFAULT 'активна' + ); + + -- Таблица товаров в корзине + CREATE TABLE IF NOT EXISTS CartItems ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + cart_id INTEGER REFERENCES Cart(id), + product_id INTEGER REFERENCES Catalog(id), + quantity INTEGER NOT NULL CHECK (quantity > 0), + price_at_moment DECIMAL(15,2) NOT NULL, + added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- ========== НОВАЯ СТРУКТУРА ЗАКАЗОВ ========== + + -- Заказы (без product_id и quantity) + CREATE TABLE IF NOT EXISTS Orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL REFERENCES Clients(id), + order_date DATE NOT NULL, + total_amount DECIMAL(15,2) NOT NULL DEFAULT 0, + status VARCHAR(50) DEFAULT 'Новый', + discount_percent INTEGER DEFAULT 0, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Позиции заявок + CREATE TABLE IF NOT EXISTS OrderItems ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL REFERENCES Orders(id) ON DELETE CASCADE, + product_id INTEGER NOT NULL REFERENCES Catalog(id), + quantity INTEGER NOT NULL CHECK (quantity > 0), + price_at_moment DECIMAL(15,2) NOT NULL, + transport_cost DECIMAL(15,2) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Индексы для оптимизации + CREATE INDEX IF NOT EXISTS idx_client_type ON Clients(contractor_type_id); + CREATE INDEX IF NOT EXISTS idx_client_inn ON Clients(inn); + CREATE INDEX IF NOT EXISTS idx_catalog_category ON Catalog(category_id); + CREATE INDEX IF NOT EXISTS idx_catalog_material ON Catalog(material_id); + CREATE INDEX IF NOT EXISTS idx_catalog_article ON Catalog(article); + CREATE INDEX IF NOT EXISTS idx_material_name ON Materials(material_name); + CREATE INDEX IF NOT EXISTS idx_cart_client ON Cart(client_id); + CREATE INDEX IF NOT EXISTS idx_cart_status ON Cart(status); + CREATE INDEX IF NOT EXISTS idx_cart_items_cart ON CartItems(cart_id); + CREATE INDEX IF NOT EXISTS idx_order_client ON Orders(client_id); + CREATE INDEX IF NOT EXISTS idx_order_date ON Orders(order_date); + CREATE INDEX IF NOT EXISTS idx_order_status ON Orders(status); + CREATE INDEX IF NOT EXISTS idx_orderitems_order ON OrderItems(order_id); + CREATE INDEX IF NOT EXISTS idx_orderitems_product ON OrderItems(product_id); + ''') + + db_connection.commit() + print("✅ Таблицы успешно созданы (новая структура с OrderItems)") + + +def calculate_discount(total_amount): + """Рассчитывает скидку на основе общей суммы заказов""" + if total_amount >= 500000: + return 20 + elif total_amount >= 300000: + return 15 + elif total_amount >= 150000: + return 10 + return 0 + + +def get_clients(db_path=DB_PATH, sort_by='rating', ascending=False): + """Получает данные о клиентах с возможностью сортировки""" + try: + conn = sqlite3.connect(db_path) + query = """ + SELECT + ct.type_name AS "Тип клиента", + c.name AS "Наименование клиента", + c.director AS "Директор", + c.email AS "Электронная почта", + c.phone AS "Телефон", + c.address AS "Адрес", + c.inn AS "ИНН", + c.rating AS "Рейтинг", + c.created_at AS "Дата создания", + COALESCE(SUM(o.total_amount), 0) as total_amount, + COUNT(DISTINCT o.id) as orders_count + FROM Clients c + JOIN ContractorType ct ON c.contractor_type_id = ct.id + LEFT JOIN Orders o ON c.id = o.client_id + GROUP BY c.id + """ + + if sort_by == 'discount': + query += " ORDER BY total_amount" + elif sort_by == 'rating': + query += " ORDER BY c.rating" + elif sort_by == 'name': + query += " ORDER BY c.name" + elif sort_by == 'type': + query += " ORDER BY ct.type_name, c.name" + else: + query += " ORDER BY c.rating" + + query += " DESC" if not ascending else " ASC" + + df = pd.read_sql_query(query, conn) + conn.close() + + if not df.empty: + df['Скидка_процент'] = df['total_amount'].apply(calculate_discount) + df['Скидка_текст'] = df['Скидка_процент'].apply(lambda x: f'{x}%') + df['Общая_сумма'] = df['total_amount'].apply(lambda x: f"{x:,.2f} руб.") + + return df + + except Exception as e: + print(f"❌ Ошибка: {e}") + return None + + +def get_orders_history(db_path=DB_PATH, limit=50, sort_by='date_desc'): + """Получает историю заказов""" + try: + conn = sqlite3.connect(db_path) + query = """ + SELECT + o.order_date AS "Дата заказа", + c.name AS "Клиент", + COUNT(oi.id) AS "Кол-во позиций", + o.total_amount AS "Сумма заказа", + o.status AS "Статус", + o.discount_percent AS "Скидка", + GROUP_CONCAT(cat.product_name, ', ') AS "Товары" + FROM Orders o + JOIN Clients c ON o.client_id = c.id + LEFT JOIN OrderItems oi ON o.id = oi.order_id + LEFT JOIN Catalog cat ON oi.product_id = cat.id + GROUP BY o.id + """ + + if sort_by == 'date_desc': + query += " ORDER BY o.order_date DESC, o.created_at DESC" + elif sort_by == 'date_asc': + query += " ORDER BY o.order_date ASC, o.created_at ASC" + elif sort_by == 'amount_desc': + query += " ORDER BY o.total_amount DESC" + else: + query += " ORDER BY o.order_date DESC, o.created_at DESC" + + query += " LIMIT ?" + + df = pd.read_sql_query(query, conn, params=(limit,)) + conn.close() + return df + except Exception as e: + print(f"❌ Ошибка при получении истории заказов: {e}") + return None + + +def print_client_cards(clients_df): + """Выводит карточки клиентов""" + if clients_df is None or clients_df.empty: + print("📭 Нет данных о клиентах") + return + + for index, row in clients_df.iterrows(): + print("╔" + "═" * 79 + "╗") + print(f"║ {'КАРТОЧКА КЛИЕНТА':^77} ║") + print("╠" + "═" * 79 + "╣") + + print(f"║ Тип: {row['Тип клиента']:<70} ║") + print(f"║ Наименование: {row['Наименование клиента']:<63} ║") + print(f"║ Директор: {row['Директор']:<68} ║") + print(f"║ Рейтинг: {row['Рейтинг']:<69} ║") + print(f"║ Заказов: {row['orders_count']:<69} ║") + print(f"║ Сумма заказов: {row['total_amount']:,.2f} руб. ║") + print(f"║ Скидка: {row['Скидка_текст']:<70} ║") + print(f"║ {'':<77} ║") + print(f"║ Email: {row['Электронная почта']:<70} ║") + print(f"║ Телефон: {row['Телефон']:<69} ║") + print(f"║ ИНН: {row['ИНН']:<73} ║") + + address = row['Адрес'] if row['Адрес'] else 'Не указан' + if len(address) > 70: + address = address[:67] + "..." + print(f"║ Адрес: {address:<70} ║") + print("╚" + "═" * 79 + "╝") + print() + + +def print_orders_cards(orders_df): + """Выводит карточки заказов""" + if orders_df is None or orders_df.empty: + print("📭 Нет данных о заказах") + return + + print("╔" + "═" * 120 + "╗") + print(f"║ {'ИСТОРИЯ ЗАКАЗОВ':^118} ║") + print("╠" + "═" * 120 + "╣") + + for index, row in orders_df.iterrows(): + date = str(row['Дата заказа'])[:10] + client = row['Клиент'][:20] if len(row['Клиент']) > 20 else row['Клиент'] + items = row['Кол-во позиций'] + amount = f"{row['Сумма заказа']:,.2f}" + status = row['Статус'] + discount = f"{row['Скидка']}%" + products = row['Товары'][:50] if row['Товары'] else '-' + + print(f"║ {date} | {client:<20} | Позиций: {items:2} | Скидка: {discount:3} | {amount:>15} руб. | {status} ║") + print(f"║ Товары: {products:<106} ║") + print("╠" + "═" * 120 + "╣") + + total_amount = orders_df['Сумма заказа'].sum() + print(f"║ {'ИТОГО:':<20} {len(orders_df)} заказов, общая сумма: {total_amount:,.2f} руб. {'':>30} ║") + print("╚" + "═" * 120 + "╝") + + +def simple_choice(options): + """Упрощенная функция выбора""" + print("\n" + "=" * 60) + print("ВЫБЕРИТЕ ОПЦИЮ:") + print("=" * 60) + for i, option in enumerate(options, 1): + print(f"{i}. {option}") + + while True: + try: + choice = input(f"\nВведите номер (1-{len(options)}): ").strip() + if choice.isdigit(): + choice_num = int(choice) + if 1 <= choice_num <= len(options): + return options[choice_num - 1] + print(f"❌ Пожалуйста, введите число от 1 до {len(options)}") + except (ValueError, EOFError): + print("❌ Неверный ввод. Попробуйте еще раз.") + except KeyboardInterrupt: + print("\n👋 Программа прервана пользователем.") + sys.exit(0) + + +def check_database(): + """Проверяет загруженные данные в базе""" + conn = sqlite3.connect(DB_PATH) + try: + cursor = conn.cursor() + print("\n🔍 Проверка загруженных данных:") + tables = ['ContractorType', 'Clients', 'Categories', 'Materials', 'Catalog', 'Cart', 'CartItems', 'Orders', 'OrderItems'] + for table in tables: + cursor.execute(f"SELECT COUNT(*) FROM {table}") + count = cursor.fetchone()[0] + print(f" {table}: {count} записей") + finally: + conn.close() + + +def load_data_from_files(): + """Загружает данные из Excel файлов в базу данных""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + try: + print("\n📁 Введите имена файлов:") + clients_file = input("Файл с клиентами (Enter = Clients_import.xlsx): ").strip() or "Clients_import.xlsx" + categories_file = input("Файл с категориями (Enter = Categories_import.xlsx): ").strip() or "Categories_import.xlsx" + materials_file = input("Файл с материалами (Enter = Materials_import.xlsx): ").strip() or "Materials_import.xlsx" + catalog_file = input("Файл с каталогом (Enter = Products_import.xlsx): ").strip() or "Products_import.xlsx" + orders_file = input("Файл с заказами (Enter = Orders_import.xlsx): ").strip() or "Orders_import.xlsx" + + # Формируем пути + clients_file = os.path.join(RESOURCE_DIR, os.path.basename(clients_file)) + categories_file = os.path.join(RESOURCE_DIR, os.path.basename(categories_file)) + materials_file = os.path.join(RESOURCE_DIR, os.path.basename(materials_file)) + catalog_file = os.path.join(RESOURCE_DIR, os.path.basename(catalog_file)) + orders_file = os.path.join(RESOURCE_DIR, os.path.basename(orders_file)) + + # 1. Загрузка типов контрагентов + print("\n1. 📊 Загрузка типов контрагентов...") + clients_df = pd.read_excel(clients_file) + + type_column = 'Тип партнера' + if type_column in clients_df.columns: + contractor_types = clients_df[type_column].unique() + for contractor_type in contractor_types: + cursor.execute("INSERT OR IGNORE INTO ContractorType (type_name) VALUES (?)", (contractor_type,)) + print(f" Загружено {len(contractor_types)} типов") + + # 2. Загрузка клиентов + print("2. 👥 Загрузка клиентов...") + loaded_clients = 0 + for _, row in clients_df.iterrows(): + type_value = row.get('Тип партнера', 'Клиент') + cursor.execute("SELECT id FROM ContractorType WHERE type_name = ?", (type_value,)) + result = cursor.fetchone() + contractor_type_id = result[0] if result else 1 + + cursor.execute("""INSERT OR IGNORE INTO Clients + (contractor_type_id, name, director, email, phone, address, inn, rating) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + (contractor_type_id, + row.get('Наименование партнера', ''), + row.get('Директор', ''), + row.get('Электронная почта партнера', ''), + row.get('Телефон партнера', ''), + row.get('Юридический адрес партнера', ''), + str(row.get('ИНН', '')), + float(row.get('Рейтинг', 0))) + ) + loaded_clients += 1 + print(f" Загружено {loaded_clients} клиентов") + + # 3. Загрузка категорий + print("3. 📦 Загрузка категорий...") + categories_df = pd.read_excel(categories_file) + loaded_categories = 0 + for _, row in categories_df.iterrows(): + cursor.execute("INSERT OR IGNORE INTO Categories (category_name, coefficient) VALUES (?, ?)", + (row.get('Тип продукции', ''), + float(row.get('Коэффициент типа продукции', 1.0))) + ) + loaded_categories += 1 + print(f" Загружено {loaded_categories} категорий") + + # 4. Загрузка материалов + print("4. 📦 Загрузка материалов...") + materials_df = pd.read_excel(materials_file) + loaded_materials = 0 + for _, row in materials_df.iterrows(): + cursor.execute(""" + INSERT OR IGNORE INTO Materials + (material_name, description, category, color, density, thickness, + supplier, country, wear_class, price_per_unit, unit, in_stock) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + row.get('Наименование материала', ''), + row.get('Описание', ''), + row.get('Категория', ''), + row.get('Цвет', ''), + row.get('Плотность', None), + row.get('Толщина', None), + row.get('Поставщик', ''), + row.get('Страна', ''), + row.get('Класс прочности', ''), + float(row.get('Цена за ед.', 0)), + row.get('Единица', 'шт'), + int(row.get('В наличии', 0)) + )) + loaded_materials += 1 + print(f" ✅ Загружено {loaded_materials} материалов") + + # 5. Загрузка каталога + print("5. 🏷️ Загрузка каталога...") + catalog_df = pd.read_excel(catalog_file) + loaded_products = 0 + for _, row in catalog_df.iterrows(): + cursor.execute(""" + INSERT OR IGNORE INTO Catalog + (category_id, material_id, product_name, article, description, min_price, quantity_in_stock) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + int(row.get('category_id', 1)), + int(row.get('material_id')) if pd.notna(row.get('material_id')) else None, + row.get('product_name', ''), + str(row.get('article', '')), + row.get('description', ''), + float(row.get('min_price', 0)), + int(row.get('quantity_in_stock', 0)) + )) + loaded_products += 1 + print(f" ✅ Загружено {loaded_products} товаров") + + # 6. Загрузка заказов + print("6. 📝 Загрузка заказов...") + + # Получаем соответствие товаров + products_dict = {} + cursor.execute("SELECT id, product_name, article, min_price FROM Catalog") + for row in cursor.fetchall(): + products_dict[row[1]] = {'id': row[0], 'price': row[3]} + + # Получаем соответствие поставщиков + suppliers_dict = {} + cursor.execute("SELECT id, name FROM Clients") + for row in cursor.fetchall(): + suppliers_dict[row[1]] = row[0] + + # Читаем заказы + orders_df = pd.read_excel(orders_file) + + # Группируем заказы + order_groups = {} + for _, row in orders_df.iterrows(): + supplier_name = row.get('Наименование партнера', '') + date = pd.to_datetime(row.get('Дата', datetime.now())).strftime('%Y-%m-%d') + key = f"{supplier_name}_{date}" + + if key not in order_groups: + order_groups[key] = { + 'supplier_id': suppliers_dict.get(supplier_name), + 'date': date, + 'items': [] + } + + product_name = row.get('Продукция', '') + quantity = int(row.get('Количество', 1)) + + if product_name in products_dict: + order_groups[key]['items'].append({ + 'product_id': products_dict[product_name]['id'], + 'quantity': quantity, + 'price': products_dict[product_name]['price'] + }) + + # Создаем заказы + orders_count = 0 + items_count = 0 + + for key, data in order_groups.items(): + if not data['supplier_id'] or not data['items']: + continue + + # Считаем сумму + total = 0 + for item in data['items']: + total += item['quantity'] * item['price'] * 1.1 + + # Создаем заказ + cursor.execute(''' + INSERT INTO Orders (client_id, order_date, total_amount, status) + VALUES (?, ?, ?, ?) + ''', (data['supplier_id'], data['date'], total, 'Выполнен')) + + order_id = cursor.lastrowid + orders_count += 1 + + # Создаем позиции + for item in data['items']: + transport = item['quantity'] * item['price'] * 0.1 + cursor.execute(''' + INSERT INTO OrderItems (order_id, product_id, quantity, price_at_moment, transport_cost) + VALUES (?, ?, ?, ?, ?) + ''', (order_id, item['product_id'], item['quantity'], item['price'], transport)) + items_count += 1 + + conn.commit() + print(f" ✅ Загружено {orders_count} заказов и {items_count} позиций") + + check_database() + + except Exception as e: + print(f"❌ Ошибка при загрузке данных: {e}") + import traceback + traceback.print_exc() + conn.rollback() + finally: + conn.close() + + +def display_menu(): + """Главное меню программы""" + menu_options = [ + 'КАРТОЧКИ КЛИЕНТОВ (сортировка по рейтингу)', + 'КАРТОЧКИ КЛИЕНТОВ (сортировка по скидкам)', + 'ИСТОРИЯ ЗАКАЗОВ', + 'ЗАПОЛНЕНИЕ БД из файлов', + 'ВЫХОД' + ] + + while True: + choice = simple_choice(menu_options) + + if choice == 'КАРТОЧКИ КЛИЕНТОВ (сортировка по рейтингу)': + print("\n🎯 КАРТОЧКИ КЛИЕНТОВ (сортировка по рейтингу)") + clients = get_clients(sort_by='rating', ascending=False) + print_client_cards(clients) + + elif choice == 'КАРТОЧКИ КЛИЕНТОВ (сортировка по скидкам)': + print("\n📊 КАРТОЧКИ КЛИЕНТОВ (сортировка по скидкам)") + clients = get_clients(sort_by='discount', ascending=False) + print_client_cards(clients) + + elif choice == 'ИСТОРИЯ ЗАКАЗОВ': + print("\n📈 ИСТОРИЯ ЗАКАЗОВ") + orders = get_orders_history(limit=50) + print_orders_cards(orders) + + elif choice == 'ЗАПОЛНЕНИЕ БД из файлов': + if os.path.exists(DB_PATH): + os.remove(DB_PATH) + print("🗑️ База данных materials.db удалена") + else: + print("ℹ️ База данных не найдена, создаем новую") + + conn = sqlite3.connect(DB_PATH) + create_tables(conn) + conn.close() + load_data_from_files() + + elif choice == 'ВЫХОД': + print("\n👋 До свидания!") + break + + +if __name__ == "__main__": + display_menu() \ No newline at end of file diff --git a/main_flask.py b/main_flask.py new file mode 100644 index 0000000..051d9ad --- /dev/null +++ b/main_flask.py @@ -0,0 +1,999 @@ +from flask import Flask, render_template, send_from_directory, request, redirect, url_for, flash, session +import sqlite3 +import os +from datetime import datetime + +app = Flask(__name__) +app.secret_key = 'my_secret_key_12345' + +# ========== ПОДКЛЮЧЕНИЕ К БАЗЕ ДАННЫХ ========== +def get_db_connection(): + conn = sqlite3.connect('materials.db') + conn.row_factory = sqlite3.Row + return conn + +# ========== ФУНКЦИИ РАСЧЕТА СКИДОК И СТОИМОСТИ ========== + +def calculate_supplier_discount(supplier_id, year): + """Расчет накопительной скидки поставщика за год (по сумме заказов)""" + conn = get_db_connection() + + # Получаем все заказы поставщика за указанный год + orders = conn.execute(''' + SELECT total_amount FROM Orders + WHERE client_id = ? AND strftime('%Y', order_date) = ? + ''', (supplier_id, str(year))).fetchall() + + conn.close() + + total_year = sum(order['total_amount'] for order in orders) + + if total_year >= 500000: + return 20 + elif total_year >= 300000: + return 15 + elif total_year >= 150000: + return 10 + return 0 + +def calculate_order_total_with_transport(items): + """Расчет общей суммы заказа с транспортными расходами (10%)""" + total = 0 + for item in items: + total += item['quantity'] * item['price'] * 1.1 + return round(total, 2) + +# ========== ГЛАВНАЯ СТРАНИЦА (ПОСТАВЩИКИ) ========== + +@app.route('/') +def index(): + # Получаем параметры фильтрации из URL + search_query = request.args.get('search', '') + client_type = request.args.get('type', 'all') + min_rating = request.args.get('rating', '0') + sort_by = request.args.get('sort', 'name_asc') + + conn = get_db_connection() + + try: + # Получаем общее количество всех клиентов (без фильтров) + total_count = conn.execute('SELECT COUNT(*) as count FROM Clients').fetchone()['count'] + + # Базовый SQL запрос для клиентов + sql = ''' + SELECT c.*, ct.type_name as contractor_type_name, + COALESCE(SUM(o.total_amount), 0) as total_amount, + COUNT(DISTINCT o.id) as orders_count + FROM Clients c + LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id + LEFT JOIN Orders o ON c.id = o.client_id + WHERE 1=1 + ''' + params = [] + + # Поиск по названию или ИНН + if search_query: + sql += " AND (c.name LIKE ? OR c.inn LIKE ?)" + params.extend([f'%{search_query}%', f'%{search_query}%']) + + # Фильтр по типу клиента + if client_type != 'all': + sql += " AND ct.type_name = ?" + params.append(client_type) + + # Фильтр по минимальному рейтингу + if min_rating != '0': + sql += " AND c.rating >= ?" + params.append(float(min_rating)) + + # Группировка (обязательно после WHERE) + sql += " GROUP BY c.id" + + # Сортировка + if sort_by == 'name_asc': + sql += " ORDER BY c.name ASC" + elif sort_by == 'name_desc': + sql += " ORDER BY c.name DESC" + elif sort_by == 'rating_desc': + sql += " ORDER BY c.rating DESC" + elif sort_by == 'rating_asc': + sql += " ORDER BY c.rating ASC" + elif sort_by == 'discount_desc': + sql += " ORDER BY total_amount DESC" + elif sort_by == 'discount_asc': + sql += " ORDER BY total_amount ASC" + else: + sql += " ORDER BY c.name ASC" + + # Выполняем запрос + clients = conn.execute(sql, params).fetchall() + + # Добавляем информацию о скидке на основе суммы + clients_with_discount = [] + for client in clients: + total_amount = client['total_amount'] or 0 + + # Рассчитываем текущую скидку на основе истории заказов + current_discount = calculate_supplier_discount(client['id'], datetime.now().year) + + client_dict = dict(client) + client_dict['total_amount'] = total_amount + client_dict['total_sales'] = total_amount + client_dict['orders_count'] = client['orders_count'] or 0 + client_dict['discount_percent'] = current_discount + client_dict['discount'] = current_discount + client_dict['discount_text'] = f'{current_discount}%' + + clients_with_discount.append(client_dict) + + conn.close() + + return render_template('index.html', + partners=clients_with_discount, + total_count=total_count) + + except sqlite3.OperationalError as e: + conn.close() + return render_template('index.html', + partners=[], + total_count=0, + error_message=f"Ошибка базы данных: {str(e)}") + +# ========== МАРШРУТЫ ДЛЯ КЛИЕНТОВ ========== + +@app.route('/add_client', methods=['GET', 'POST']) +def add_client(): + if request.method == 'POST': + try: + name = request.form['name'] + director = request.form.get('director', '') + email = request.form['email'] + phone = request.form.get('phone', '') + address = request.form.get('address', '') + inn = request.form.get('inn', '') + contractor_type_id = request.form['contractor_type_id'] + rating = request.form.get('rating', 0.0) + + conn = get_db_connection() + + # Проверяем уникальность ИНН + if inn: + existing = conn.execute('SELECT id FROM Clients WHERE inn = ?', (inn,)).fetchone() + if existing: + flash('Клиент с таким ИНН уже существует', 'error') + conn.close() + return redirect(url_for('add_client')) + + conn.execute(''' + INSERT INTO Clients (name, director, email, phone, address, inn, contractor_type_id, rating) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', (name, director, email, phone, address, inn, contractor_type_id, rating)) + + conn.commit() + conn.close() + flash('Клиент успешно добавлен!', 'success') + return redirect(url_for('index')) + + except Exception as e: + flash(f'Ошибка: {str(e)}', 'error') + return redirect(url_for('add_client')) + + # GET запрос + conn = get_db_connection() + contractor_types = conn.execute('SELECT * FROM ContractorType ORDER BY type_name').fetchall() + conn.close() + return render_template('add_client.html', contractor_types=contractor_types) + +@app.route('/edit_client/', methods=['GET', 'POST']) +def edit_client(client_id): + conn = get_db_connection() + + if request.method == 'POST': + try: + name = request.form['name'] + director = request.form.get('director', '') + email = request.form['email'] + phone = request.form.get('phone', '') + address = request.form.get('address', '') + inn = request.form.get('inn', '') + contractor_type_id = request.form['contractor_type_id'] + rating = request.form.get('rating', 0.0) + + # Проверяем уникальность ИНН + if inn: + existing = conn.execute( + 'SELECT id FROM Clients WHERE inn = ? AND id != ?', + (inn, client_id) + ).fetchone() + if existing: + flash('Клиент с таким ИНН уже существует', 'error') + conn.close() + return redirect(url_for('edit_client', client_id=client_id)) + + conn.execute(''' + UPDATE Clients + SET name = ?, director = ?, email = ?, phone = ?, + address = ?, inn = ?, contractor_type_id = ?, rating = ? + WHERE id = ? + ''', (name, director, email, phone, address, inn, contractor_type_id, rating, client_id)) + + conn.commit() + conn.close() + flash('Данные обновлены!', 'success') + return redirect(url_for('index')) + + except Exception as e: + flash(f'Ошибка: {str(e)}', 'error') + conn.close() + return redirect(url_for('edit_client', client_id=client_id)) + + # GET запрос + client = conn.execute(''' + SELECT c.*, ct.type_name as contractor_type_name + FROM Clients c + LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id + WHERE c.id = ? + ''', (client_id,)).fetchone() + + contractor_types = conn.execute('SELECT * FROM ContractorType ORDER BY type_name').fetchall() + conn.close() + + if not client: + flash('Клиент не найден', 'error') + return redirect(url_for('index')) + + return render_template('edit_client.html', client=client, contractor_types=contractor_types) + +# ========== МАРШРУТЫ ДЛЯ ЗАКАЗОВ ========== + +@app.route('/order_history/') +def order_history(client_id): + conn = get_db_connection() + + client = conn.execute(''' + SELECT c.*, ct.type_name as contractor_type_name + FROM Clients c + LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id + WHERE c.id = ? + ''', (client_id,)).fetchone() + + if not client: + conn.close() + flash('Клиент не найден', 'error') + return redirect(url_for('index')) + + # Получаем все заказы + orders = conn.execute(''' + SELECT o.* + FROM Orders o + WHERE o.client_id = ? + ORDER BY o.order_date DESC, o.id DESC + ''', (client_id,)).fetchall() + + orders_with_items = [] + + for order in orders: + order_dict = dict(order) + + # Получаем позиции для заказа + items = conn.execute(''' + SELECT oi.*, cat.product_name, cat.article, + cat2.category_name, m.material_name + FROM OrderItems oi + JOIN Catalog cat ON oi.product_id = cat.id + LEFT JOIN Categories cat2 ON cat.category_id = cat2.id + LEFT JOIN Materials m ON cat.material_id = m.id + WHERE oi.order_id = ? + ''', (order['id'],)).fetchall() + + # Рассчитываем базовую стоимость (сумма quantity * price_at_moment) + base_amount = 0 + items_list = [] + + for item in items: + item_dict = dict(item) + # Рассчитываем стоимость позиции + item_total = item_dict['quantity'] * item_dict['price_at_moment'] + item_dict['item_total'] = item_total + base_amount += item_total + items_list.append(item_dict) + + # Транспортные расходы (10% от базовой стоимости) + shipping_cost = base_amount * 0.1 + + # Получаем скидку клиента + try: + if order_dict['order_date']: + order_year = datetime.strptime(order_dict['order_date'], '%Y-%m-%d').year + else: + order_year = datetime.now().year + except: + order_year = datetime.now().year + + discount = calculate_supplier_discount(client_id, order_year) + + # Рассчитываем итоговую стоимость с учётом скидки + total_with_shipping = base_amount + shipping_cost + total_amount_calculated = total_with_shipping * (1 - discount / 100) if discount > 0 else total_with_shipping + + # Добавляем рассчитанные поля в словарь заказа + order_dict['base_amount'] = base_amount + order_dict['shipping_cost'] = shipping_cost + order_dict['discount'] = discount + order_dict['total_amount_calculated'] = total_amount_calculated + order_dict['display_total'] = order_dict.get('total_amount', total_amount_calculated) + order_dict['order_items'] = items_list + order_dict['items_count'] = len(items_list) + + orders_with_items.append(order_dict) + + # Считаем общую статистику + total_stats = conn.execute(''' + SELECT + COUNT(DISTINCT o.id) as total_orders, + SUM(o.total_amount) as total_amount, + SUM(CASE WHEN o.status = 'Выполнен' THEN 1 ELSE 0 END) as completed_orders, + COALESCE(SUM(oi.quantity), 0) as total_quantity + FROM Orders o + LEFT JOIN OrderItems oi ON o.id = oi.order_id + WHERE o.client_id = ? + ''', (client_id,)).fetchone() + + conn.close() + + return render_template('order_history.html', + client=client, + orders=orders_with_items, + total_stats=total_stats) + +@app.route('/update_order_status/', methods=['POST']) +def update_order_status(order_id): + """Обновление статуса заказа""" + try: + new_status = request.form['status'] + + conn = get_db_connection() + conn.execute('UPDATE Orders SET status = ? WHERE id = ?', (new_status, order_id)) + conn.commit() + conn.close() + + flash(f'Статус заказа обновлен на "{new_status}"', 'success') + except Exception as e: + flash(f'Ошибка при обновлении статуса: {str(e)}', 'error') + + return redirect(request.referrer or url_for('orders_list')) + +@app.route('/add_order/', methods=['GET', 'POST']) +def add_order(client_id): + if request.method == 'POST': + try: + order_date = request.form['order_date'] + status = request.form.get('status', 'Новый') + + conn = get_db_connection() + + # Проверяем наличие таблицы OrderItems + cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='OrderItems'") + has_orderitems = cursor.fetchone() is not None + + if has_orderitems: + # Новая структура + cursor = conn.execute(''' + INSERT INTO Orders (client_id, order_date, status, total_amount) + VALUES (?, ?, ?, 0) + ''', (client_id, order_date, status)) + + order_id = cursor.lastrowid + + # Получаем позиции из формы + product_ids = request.form.getlist('product_id[]') + quantities = request.form.getlist('quantity[]') + + total = 0 + + for i in range(len(product_ids)): + if product_ids[i] and quantities[i]: + product_id = int(product_ids[i]) + quantity = int(quantities[i]) + + # Получаем цену товара + product = conn.execute('SELECT min_price FROM Catalog WHERE id = ?', (product_id,)).fetchone() + + if product: + price = product['min_price'] + + # Добавляем позицию + conn.execute(''' + INSERT INTO OrderItems (order_id, product_id, quantity, price_at_moment) + VALUES (?, ?, ?, ?) + ''', (order_id, product_id, quantity, price)) + + # Считаем сумму с транспортными расходами + total += quantity * price * 1.1 + + # Рассчитываем скидку + year = datetime.strptime(order_date, '%Y-%m-%d').year + discount = calculate_supplier_discount(client_id, year) + + # Применяем скидку + total_with_discount = total * (1 - discount/100) + + # Обновляем сумму заказа + conn.execute('UPDATE Orders SET total_amount = ? WHERE id = ?', (total_with_discount, order_id)) + + discount_msg = f' Применена накопительная скидка {discount}% (экономия {total * discount/100:.2f} руб.)' if discount > 0 else '' + + else: + # Старая структура (для совместимости) + product_id = request.form.get('product_id') + quantity = int(request.form.get('quantity', 1)) + + product = conn.execute('SELECT min_price FROM Catalog WHERE id = ?', (product_id,)).fetchone() + + if not product: + flash('Товар не найден', 'error') + return redirect(url_for('add_order', client_id=client_id)) + + base_amount = product['min_price'] * quantity + total_amount = base_amount * 1.1 # с транспортом + + cursor = conn.execute(''' + INSERT INTO Orders (client_id, product_id, quantity, order_date, total_amount, status) + VALUES (?, ?, ?, ?, ?, ?) + ''', (client_id, product_id, quantity, order_date, total_amount, status)) + + order_id = cursor.lastrowid + discount_msg = '' + + conn.commit() + conn.close() + + flash(f'Заказ успешно добавлен!{discount_msg}', 'success') + return redirect(url_for('order_history', client_id=client_id)) + + except Exception as e: + flash(f'Ошибка при добавлении заказа: {str(e)}', 'error') + return redirect(url_for('add_order', client_id=client_id)) + + # GET запрос + conn = get_db_connection() + + client = conn.execute(''' + SELECT c.*, ct.type_name as contractor_type_name + FROM Clients c + LEFT JOIN ContractorType ct ON c.contractor_type_id = ct.id + WHERE c.id = ? + ''', (client_id,)).fetchone() + + products = conn.execute(''' + SELECT cat.*, c.category_name, m.material_name + FROM Catalog cat + LEFT JOIN Categories c ON cat.category_id = c.id + LEFT JOIN Materials m ON cat.material_id = m.id + ORDER BY c.category_name, cat.product_name + ''').fetchall() + + conn.close() + + if not client: + flash('Клиент не найден', 'error') + return redirect(url_for('index')) + + today = datetime.now().strftime('%Y-%m-%d') + + return render_template('add_order.html', client=client, products=products, today=today) + +@app.route('/orders_list') +def orders_list(): + """Список всех заказов""" + conn = get_db_connection() + + # Получаем всех клиентов для выпадающего списка + clients = conn.execute(''' + SELECT id, name FROM Clients ORDER BY name + ''').fetchall() + + # Проверяем наличие таблицы OrderItems + cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='OrderItems'") + has_orderitems = cursor.fetchone() is not None + + if has_orderitems: + # Новая структура + orders = conn.execute(''' + SELECT + o.id, + o.client_id, + o.order_date, + o.total_amount, + o.status, + c.name as client_name, + c.inn as client_inn, + COUNT(oi.id) as items_count, + SUM(oi.quantity) as total_quantity, + GROUP_CONCAT(cat.product_name, ', ') as products_list + FROM Orders o + LEFT JOIN Clients c ON o.client_id = c.id + LEFT JOIN OrderItems oi ON o.id = oi.order_id + LEFT JOIN Catalog cat ON oi.product_id = cat.id + GROUP BY o.id + ORDER BY o.order_date DESC, o.id DESC + LIMIT 100 + ''').fetchall() + + # Считаем общую статистику + total_stats = conn.execute(''' + SELECT + COUNT(*) as total_orders, + SUM(o.total_amount) as total_amount, + SUM(CASE WHEN o.status = 'Выполнен' THEN 1 ELSE 0 END) as completed_orders + FROM Orders o + ''').fetchone() + + else: + # Старая структура (с product_id и quantity в Orders) + orders = conn.execute(''' + SELECT + o.id, + o.client_id, + o.order_date, + o.total_amount, + o.status, + o.quantity, + c.name as client_name, + c.inn as client_inn, + cat.product_name, + cat.article, + cat2.category_name + FROM Orders o + LEFT JOIN Clients c ON o.client_id = c.id + LEFT JOIN Catalog cat ON o.product_id = cat.id + LEFT JOIN Categories cat2 ON cat.category_id = cat2.id + ORDER BY o.order_date DESC, o.id DESC + LIMIT 100 + ''').fetchall() + + # Считаем общую статистику + total_stats = conn.execute(''' + SELECT + COUNT(*) as total_orders, + SUM(quantity) as total_quantity, + SUM(total_amount) as total_amount, + SUM(CASE WHEN status = 'Выполнен' THEN 1 ELSE 0 END) as completed_orders + FROM Orders + ''').fetchone() + + conn.close() + + return render_template('orders_list.html', orders=orders, total_stats=total_stats, clients=clients, has_orderitems=has_orderitems) + +# ========== МАРШРУТ ДЛЯ КАТАЛОГА ========== + +@app.route('/catalog') +def catalog(): + conn = get_db_connection() + + # Получаем список всех категорий для фильтра + categories = conn.execute('SELECT * FROM Categories ORDER BY category_name').fetchall() + + # Получаем список всех поставщиков для фильтра + suppliers = conn.execute('SELECT id, name FROM Clients ORDER BY name').fetchall() + + # Получаем параметры фильтрации из URL + search_query = request.args.get('search', '') + category_id = request.args.get('category', 'all') + supplier_id = request.args.get('supplier', 'all') + sort_by = request.args.get('sort', 'name_asc') + + # Базовый SQL запрос с добавлением поставщика + sql = ''' + SELECT cat.*, c.category_name, m.material_name, m.color, + cl.name as supplier_name, cl.id as supplier_id + FROM Catalog cat + LEFT JOIN Categories c ON cat.category_id = c.id + LEFT JOIN Materials m ON cat.material_id = m.id + LEFT JOIN Clients cl ON m.supplier = cl.name + WHERE 1=1 + ''' + params = [] + + # Поиск по названию или артикулу + if search_query: + sql += " AND (cat.product_name LIKE ? OR cat.article LIKE ?)" + params.extend([f'%{search_query}%', f'%{search_query}%']) + + # Фильтр по категории + if category_id != 'all': + sql += " AND cat.category_id = ?" + params.append(category_id) + + # Фильтр по поставщику + if supplier_id != 'all': + sql += " AND cl.id = ?" + params.append(supplier_id) + + # Сортировка + if sort_by == 'name_asc': + sql += " ORDER BY cat.product_name ASC" + elif sort_by == 'name_desc': + sql += " ORDER BY cat.product_name DESC" + elif sort_by == 'price_asc': + sql += " ORDER BY cat.min_price ASC" + elif sort_by == 'price_desc': + sql += " ORDER BY cat.min_price DESC" + elif sort_by == 'stock_desc': + sql += " ORDER BY cat.quantity_in_stock DESC" + else: + sql += " ORDER BY cat.product_name ASC" + + # Выполняем запрос + products = conn.execute(sql, params).fetchall() + + # Получаем количество товаров в корзине для бейджа + cart_count = 0 + if 'cart' in session: + cart_count = len(session['cart']) + + conn.close() + + return render_template('catalog.html', + products=products, + categories=categories, + suppliers=suppliers, + cart_count=cart_count) + +# ========== МАРШРУТЫ ДЛЯ УПРАВЛЕНИЯ ТОВАРАМИ ========== + +@app.route('/add_product', methods=['GET', 'POST']) +def add_product(): + """Добавление нового товара""" + if request.method == 'POST': + try: + product_name = request.form['product_name'] + article = request.form['article'] + category_id = request.form['category_id'] + material_id = request.form.get('material_id') or None + description = request.form.get('description', '') + min_price = float(request.form['min_price']) + quantity_in_stock = int(request.form.get('quantity_in_stock', 0)) + + conn = get_db_connection() + + # Проверяем уникальность артикула + existing = conn.execute('SELECT id FROM Catalog WHERE article = ?', (article,)).fetchone() + if existing: + flash('Товар с таким артикулом уже существует', 'error') + conn.close() + return redirect(url_for('add_product')) + + conn.execute(''' + INSERT INTO Catalog (product_name, article, category_id, material_id, description, min_price, quantity_in_stock) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (product_name, article, category_id, material_id, description, min_price, quantity_in_stock)) + + conn.commit() + conn.close() + + flash('Товар успешно добавлен!', 'success') + return redirect(url_for('catalog')) + + except Exception as e: + flash(f'Ошибка: {str(e)}', 'error') + return redirect(url_for('add_product')) + + # GET запрос + conn = get_db_connection() + categories = conn.execute('SELECT * FROM Categories ORDER BY category_name').fetchall() + materials = conn.execute('SELECT * FROM Materials ORDER BY material_name').fetchall() + conn.close() + + return render_template('add_product.html', categories=categories, materials=materials) + +@app.route('/edit_product/', methods=['GET', 'POST']) +def edit_product(product_id): + """Редактирование товара""" + conn = get_db_connection() + + if request.method == 'POST': + try: + product_name = request.form['product_name'] + article = request.form['article'] + category_id = request.form['category_id'] + material_id = request.form.get('material_id') or None + description = request.form.get('description', '') + min_price = float(request.form['min_price']) + quantity_in_stock = int(request.form.get('quantity_in_stock', 0)) + + # Проверяем уникальность артикула (исключая текущий товар) + existing = conn.execute( + 'SELECT id FROM Catalog WHERE article = ? AND id != ?', + (article, product_id) + ).fetchone() + if existing: + flash('Товар с таким артикулом уже существует', 'error') + conn.close() + return redirect(url_for('edit_product', product_id=product_id)) + + conn.execute(''' + UPDATE Catalog + SET product_name = ?, article = ?, category_id = ?, material_id = ?, + description = ?, min_price = ?, quantity_in_stock = ? + WHERE id = ? + ''', (product_name, article, category_id, material_id, description, min_price, quantity_in_stock, product_id)) + + conn.commit() + conn.close() + + flash('Товар успешно обновлен!', 'success') + return redirect(url_for('catalog')) + + except Exception as e: + flash(f'Ошибка: {str(e)}', 'error') + return redirect(url_for('edit_product', product_id=product_id)) + + # GET запрос + product = conn.execute(''' + SELECT cat.*, c.category_name, m.material_name + FROM Catalog cat + LEFT JOIN Categories c ON cat.category_id = c.id + LEFT JOIN Materials m ON cat.material_id = m.id + WHERE cat.id = ? + ''', (product_id,)).fetchone() + + categories = conn.execute('SELECT * FROM Categories ORDER BY category_name').fetchall() + materials = conn.execute('SELECT * FROM Materials ORDER BY material_name').fetchall() + conn.close() + + if not product: + flash('Товар не найден', 'error') + return redirect(url_for('catalog')) + + return render_template('edit_product.html', product=product, categories=categories, materials=materials) + +@app.route('/delete_product/', methods=['POST']) +def delete_product(product_id): + """Удаление товара""" + try: + conn = get_db_connection() + + # Проверяем наличие таблицы OrderItems + cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='OrderItems'") + has_orderitems = cursor.fetchone() is not None + + if has_orderitems: + # Новая структура + items = conn.execute('SELECT COUNT(*) as count FROM OrderItems WHERE product_id = ?', (product_id,)).fetchone() + if items['count'] > 0: + flash('Нельзя удалить товар, который есть в заказах', 'error') + conn.close() + return redirect(url_for('catalog')) + else: + # Старая структура + orders = conn.execute('SELECT COUNT(*) as count FROM Orders WHERE product_id = ?', (product_id,)).fetchone() + if orders['count'] > 0: + flash('Нельзя удалить товар, который есть в заказах', 'error') + conn.close() + return redirect(url_for('catalog')) + + conn.execute('DELETE FROM Catalog WHERE id = ?', (product_id,)) + conn.commit() + conn.close() + + flash('Товар успешно удален', 'success') + except Exception as e: + flash(f'Ошибка при удалении: {str(e)}', 'error') + + return redirect(url_for('catalog')) + +# ========== МАРШРУТЫ ДЛЯ КОРЗИНЫ ========== + +@app.route('/cart') +def view_cart(): + """Просмотр корзины""" + cart = session.get('cart', {}) + cart_items = [] + total_amount = 0 + + conn = get_db_connection() + + for key, item_data in cart.items(): + product_id = item_data['product_id'] + quantity = item_data['quantity'] + supplier_id = item_data.get('supplier_id') + + product = conn.execute(''' + SELECT cat.*, c.category_name, m.material_name, cat.quantity_in_stock + FROM Catalog cat + LEFT JOIN Categories c ON cat.category_id = c.id + LEFT JOIN Materials m ON cat.material_id = m.id + WHERE cat.id = ? + ''', (product_id,)).fetchone() + + if product: + # Получаем информацию о поставщике + supplier = None + if supplier_id: + supplier = conn.execute('SELECT name FROM Clients WHERE id = ?', (supplier_id,)).fetchone() + + cart_items.append({ + 'key': key, + 'product_id': product_id, + 'supplier_id': supplier_id, + 'supplier_name': supplier['name'] if supplier else 'Не указан', + 'product_name': product['product_name'], + 'article': product['article'], + 'category_name': product['category_name'], + 'material_name': product['material_name'], + 'price': product['min_price'], + 'quantity': quantity, + 'in_stock': product['quantity_in_stock'] or 0 + }) + total_amount += product['min_price'] * quantity + + conn.close() + + return render_template('cart_new.html', cart_items=cart_items, total_amount=total_amount) + +@app.route('/add_to_cart/', methods=['POST']) +def add_to_cart(product_id): + """Добавление товара в корзину с выбором поставщика""" + quantity = int(request.form.get('quantity', 1)) + supplier_id = request.form.get('supplier_id') + + if not supplier_id: + flash('Пожалуйста, выберите поставщика', 'error') + return redirect(url_for('catalog')) + + # Получаем информацию о товаре для проверки наличия + conn = get_db_connection() + product = conn.execute('SELECT quantity_in_stock FROM Catalog WHERE id = ?', (product_id,)).fetchone() + conn.close() + + if not product or product['quantity_in_stock'] < quantity: + flash('Недостаточно товара на складе', 'error') + return redirect(url_for('catalog')) + + cart = session.get('cart', {}) + + # Создаем уникальный ключ для комбинации товар + поставщик + key = f"{product_id}_{supplier_id}" + + if key in cart: + cart[key]['quantity'] += quantity + else: + cart[key] = { + 'product_id': product_id, + 'supplier_id': int(supplier_id), + 'quantity': quantity + } + + session['cart'] = cart + flash('Товар добавлен в корзину!', 'success') + + return redirect(url_for('catalog')) + +@app.route('/update_cart/', methods=['POST']) +def update_cart(item_key): + """Обновление количества товара в корзине""" + quantity = int(request.form.get('quantity', 1)) + + cart = session.get('cart', {}) + + if item_key in cart: + if quantity > 0: + cart[item_key]['quantity'] = quantity + else: + cart.pop(item_key, None) + + session['cart'] = cart + + return redirect(url_for('view_cart')) + +@app.route('/remove_from_cart/', methods=['POST']) +def remove_from_cart(item_key): + """Удаление товара из корзины""" + cart = session.get('cart', {}) + cart.pop(item_key, None) + session['cart'] = cart + + return redirect(url_for('view_cart')) + +@app.route('/checkout', methods=['POST']) +def checkout(): + """Оформление заказа из корзины""" + cart = session.get('cart', {}) + + if not cart: + flash('Корзина пуста', 'error') + return redirect(url_for('catalog')) + + # Группируем товары по поставщикам для создания отдельных заказов + orders_by_supplier = {} + + conn = get_db_connection() + + for key, item_data in cart.items(): + supplier_id = item_data['supplier_id'] + product_id = item_data['product_id'] + quantity = item_data['quantity'] + + if supplier_id not in orders_by_supplier: + orders_by_supplier[supplier_id] = [] + + # Получаем цену товара + product = conn.execute('SELECT min_price FROM Catalog WHERE id = ?', (product_id,)).fetchone() + if product: + orders_by_supplier[supplier_id].append({ + 'product_id': product_id, + 'quantity': quantity, + 'price': product['min_price'] + }) + + # Создаем заказы для каждого поставщика + orders_created = 0 + today = datetime.now().strftime('%Y-%m-%d') + + for supplier_id, items in orders_by_supplier.items(): + # Считаем общую сумму с транспортом + total = 0 + for item in items: + total += item['quantity'] * item['price'] * 1.1 + + # Рассчитываем скидку + discount = calculate_supplier_discount(supplier_id, datetime.now().year) + total_with_discount = total * (1 - discount/100) + + # Создаем заказ + cursor = conn.execute(''' + INSERT INTO Orders (client_id, order_date, total_amount, status) + VALUES (?, ?, ?, ?) + ''', (supplier_id, today, total_with_discount, 'Новый')) + + order_id = cursor.lastrowid + + # Добавляем позиции + for item in items: + conn.execute(''' + INSERT INTO OrderItems (order_id, product_id, quantity, price_at_moment) + VALUES (?, ?, ?, ?) + ''', (order_id, item['product_id'], item['quantity'], item['price'])) + + orders_created += 1 + + conn.commit() + conn.close() + + # Очищаем корзину + session.pop('cart', None) + + flash(f'Заказ успешно оформлен! Создано {orders_created} заказов.', 'success') + return redirect(url_for('orders_list')) + +# ========== МАРШРУТ ДЛЯ МАТЕРИАЛОВ ========== + +@app.route('/materials') +def materials(): + """Список всех материалов""" + conn = get_db_connection() + + materials = conn.execute(''' + SELECT * FROM Materials + ORDER BY material_name + ''').fetchall() + + conn.close() + + return render_template('materials.html', materials=materials) + +# ========== СЛУЖЕБНЫЕ МАРШРУТЫ ========== + +@app.route('/favicon.ico') +def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + +@app.route('/static/') +def static_files(filename): + return send_from_directory(app.root_path + '/static', filename) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/materials.db b/materials.db new file mode 100644 index 0000000..005f794 Binary files /dev/null and b/materials.db differ diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..1c6ea27 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,470 @@ +/* Подключаем шрифт Verdana */ +@import url('https://fonts.googleapis.com/css2?family=Verdana:wght@400;500;600;700&display=swap'); + +:root { + --antique-white: #FAEBD7; + --light-brown: #DEB887; + --brown: #A0522D; + --dark-brown: #8B4513; + --text-dark: #2c3e50; + --text-light: #5D3A1A; +} + +body { + font-family: 'Verdana', sans-serif; + background-color: var(--antique-white); + margin: 0; + padding: 0; +} + +.main-container { + background-color: #ffffff; + min-height: 100vh; + padding: 20px; +} + +.header { + background: linear-gradient(135deg, var(--brown) 0%, var(--light-brown) 100%); + padding: 25px; + border-radius: 12px; + margin-bottom: 35px; + color: white; + box-shadow: 0 6px 20px rgba(160, 82, 45, 0.3); +} + +.logo-container { + display: flex; + align-items: center; + gap: 25px; +} + +.logo { + height: 70px; + width: auto; + object-fit: contain; + filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.2)); +} + +.header-title { + font-size: 2.5em; + font-weight: 700; + margin: 0; + text-shadow: 2px 2px 6px rgba(0,0,0,0.3); + letter-spacing: 0.5px; +} + +.page-title { + color: var(--brown); + font-weight: 700; + border-bottom: 3px solid var(--light-brown); + padding-bottom: 10px; + margin-bottom: 25px; + font-family: 'Verdana', sans-serif; +} + +/* Карточки партнеров */ +.partner-card { + border: 3px solid var(--light-brown); + border-radius: 15px; + padding: 25px; + margin-bottom: 25px; + background: #ffffff; + box-shadow: 0 6px 20px rgba(222, 184, 135, 0.25); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.partner-card:hover { + transform: translateY(-5px); + box-shadow: 0 12px 25px rgba(160, 82, 45, 0.2); + border-color: var(--brown); +} + +.partner-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 3px solid var(--light-brown); + padding-bottom: 18px; + margin-bottom: 20px; +} + +.partner-name-line { + display: flex; + align-items: center; + gap: 15px; + flex-wrap: wrap; +} + +.partner-type { + font-size: 1.2em; + font-weight: 600; + color: var(--brown); + background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%); + padding: 8px 18px; + border-radius: 25px; + border: 2px solid var(--light-brown); +} + +.partner-name { + font-size: 1.6em; + font-weight: 700; + color: var(--dark-brown); + margin: 0; +} + +.divider { + color: var(--light-brown); + font-weight: bold; + font-size: 1.5em; +} + +.discount-info { + display: flex; + align-items: center; + gap: 20px; +} + +.discount-badge { + font-size: 1.2em; + padding: 10px 20px; + border-radius: 30px; + background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%); + color: white; + font-weight: 600; + box-shadow: 0 3px 10px rgba(160, 82, 45, 0.3); +} + +.sales-amount { + font-size: 1.1em; + color: var(--brown); + font-weight: 600; + background-color: var(--antique-white); + padding: 8px 15px; + border-radius: 8px; +} + +.partner-info-row { + margin-bottom: 0; + padding: 8px 0; + line-height: 1.5; + border-bottom: 1px dashed var(--light-brown); +} + +.partner-info-row:last-child { + border-bottom: none; +} + +.partner-label { + font-weight: 700; + color: var(--brown); + min-width: 140px; + display: inline-block; + font-size: 1.05em; +} + +.partner-value { + font-size: 1.05em; + color: var(--text-dark); + font-weight: 500; +} + +.partner-buttons-container { + display: flex; + gap: 15px; + justify-content: flex-start; + margin-top: 25px; + padding-top: 20px; + border-top: 3px solid var(--light-brown); +} + +.btn-action { + padding: 12px 25px; + font-size: 1em; + display: flex; + align-items: center; + gap: 10px; + min-width: 200px; + justify-content: center; + border: none; + border-radius: 10px; + font-weight: 700; + transition: all 0.3s ease; + cursor: pointer; + letter-spacing: 0.5px; + color: white; +} + +.btn-edit { + background: linear-gradient(135deg, var(--light-brown) 0%, var(--brown) 100%); +} + +.btn-add { + background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%); +} + +.btn-history { + background: linear-gradient(135deg, #8B7355 0%, var(--dark-brown) 100%); +} + +.btn-action:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(160, 82, 45, 0.25); + color: white; +} + +.btn-new-partner { + background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%); + color: white; + border: none; + padding: 14px 28px; + font-weight: 700; + border-radius: 10px; + font-size: 1.1em; + letter-spacing: 0.5px; +} + +.btn-new-partner:hover { + background: linear-gradient(135deg, var(--dark-brown) 0%, var(--brown) 100%); + color: white; + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(160, 82, 45, 0.3); +} + +.rating-badge { + font-size: 1em; + padding: 8px 16px; + border-radius: 20px; + background: linear-gradient(135deg, var(--light-brown) 0%, var(--brown) 100%); + color: white; + font-weight: 700; +} + +.alert { + border-radius: 12px; + border: none; + background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%); + border-left: 6px solid var(--brown); + color: var(--text-dark); +} + +.volume-info { + background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%); + padding: 12px 20px; + border-radius: 10px; + margin-top: 15px; + border: 2px solid var(--light-brown); +} + +.volume-label { + font-weight: 700; + color: var(--brown); + font-size: 1.1em; +} + +.volume-value { + color: var(--dark-brown); + font-weight: 700; + font-size: 1.2em; +} + +/* Стили для страницы истории */ +.table-responsive { + border-radius: 12px; + overflow: hidden; + border: 3px solid var(--light-brown); + box-shadow: 0 6px 20px rgba(222, 184, 135, 0.3); +} + +.table { + margin-bottom: 0; + border-collapse: separate; + border-spacing: 0; +} + +.table th { + background: linear-gradient(135deg, var(--brown) 0%, var(--dark-brown) 100%); + color: white; + font-weight: 700; + padding: 16px; + text-align: center; + font-size: 1.1em; + border: none; +} + +.table td { + padding: 14px; + text-align: center; + vertical-align: middle; + border-bottom: 2px solid var(--light-brown); + font-size: 1.05em; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(222, 184, 135, 0.08); +} + +.table-striped tbody tr:hover { + background-color: rgba(160, 82, 45, 0.1); + transition: background-color 0.3s ease; +} + +.table tfoot { + background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%); + font-weight: 700; + font-size: 1.1em; +} + +.table tfoot td { + border-top: 3px solid var(--brown); + padding: 18px; +} + +.text-end { + text-align: right !important; +} + +/* Стили для иконок */ +i { + color: var(--brown); + margin-right: 5px; +} + +.btn-action i, .btn-new-partner i { + color: white; +} + +/* Стили для блока с расчетом стоимости */ +.calculation-block { + background: linear-gradient(135deg, var(--antique-white) 0%, #F5DEB3 100%); + border: 3px solid var(--light-brown); + border-radius: 15px; + padding: 25px; + margin-bottom: 30px; + box-shadow: 0 6px 20px rgba(222, 184, 135, 0.25); +} + +.calculation-title { + color: var(--brown); + font-weight: 700; + border-bottom: 3px solid var(--brown); + padding-bottom: 15px; + margin-bottom: 25px; +} + +.calculation-result { + background-color: white; + border: 2px solid var(--light-brown); + border-radius: 10px; + padding: 20px; + margin-top: 20px; +} + +.result-item { + padding: 10px 0; + border-bottom: 1px dashed var(--light-brown); +} + +.result-item:last-child { + border-bottom: none; + font-weight: 700; + font-size: 1.2em; + color: var(--brown); +} + +/* Стили для фильтров и сортировки - ВАЖНО! */ +.filter-section { + background: white !important; + border: 2px solid var(--light-brown) !important; + border-radius: 12px !important; + padding: 20px !important; + margin-bottom: 25px !important; + box-shadow: 0 2px 8px rgba(160, 82, 45, 0.1) !important; +} + +.filter-title { + color: var(--brown) !important; + font-weight: 700 !important; + margin-bottom: 15px !important; + border-bottom: 2px solid var(--light-brown) !important; + padding-bottom: 10px !important; +} + +.form-control, .form-select { + border: 2px solid var(--light-brown) !important; + border-radius: 8px !important; + padding: 10px 15px !important; + font-size: 1em !important; + transition: all 0.3s ease !important; + background-color: white !important; +} + +.form-control:focus, .form-select:focus { + border-color: var(--brown) !important; + box-shadow: 0 0 0 0.25rem rgba(160, 82, 45, 0.25) !important; + outline: none !important; +} + +.form-label { + color: var(--brown) !important; + font-weight: 600 !important; + margin-bottom: 5px !important; +} + +/* Кнопки */ +.btn-primary { + background: var(--brown) !important; + color: white !important; + border: none !important; +} + +.btn-primary:hover { + background: var(--dark-brown) !important; + transform: translateY(-1px) !important; + box-shadow: 0 4px 8px rgba(160, 82, 45, 0.2) !important; +} + +.btn-secondary { + background: var(--antique-white) !important; + color: var(--brown) !important; + border: 1px solid var(--light-brown) !important; +} + +.btn-secondary:hover { + background: var(--light-brown) !important; + color: white !important; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .table-responsive { + font-size: 0.9em; + } + + .table th, .table td { + padding: 10px 6px; + } + + .btn-action { + min-width: 150px; + padding: 10px 15px; + font-size: 0.9em; + } + + .partner-card { + padding: 20px; + } + + .header-title { + font-size: 1.8em; + } +} + +/* Анимации */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.partner-card, .table-responsive, .calculation-block, .filter-section { + animation: fadeIn 0.5s ease-out; +} \ No newline at end of file diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..77a345d Binary files /dev/null and b/static/favicon.png differ diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..f5b0daa --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,37 @@ +document.addEventListener('DOMContentLoaded', function() { + // Кнопка нового партнера + const newPartnerBtn = document.querySelector('.btn-new-partner'); + if (newPartnerBtn) { + newPartnerBtn.addEventListener('click', function() { + // Переход на страницу добавления партнера + window.location.href = '/add_partner'; + }); + } + + // Кнопка "Редактирование" + document.querySelectorAll('.btn-edit').forEach(btn => { + btn.addEventListener('click', function() { + const partnerId = this.getAttribute('data-partner-id'); + // Переход на страницу редактирования + window.location.href = `/edit_partner/${partnerId}`; + }); + }); + + // Кнопка "Добавление продажи" + document.querySelectorAll('.btn-add').forEach(btn => { + btn.addEventListener('click', function() { + const partnerId = this.getAttribute('data-partner-id'); + // Переход на страницу добавления продажи + window.location.href = `/add_sale/${partnerId}`; + }); + }); + + // Кнопка "История реализации" + document.querySelectorAll('.btn-history').forEach(btn => { + btn.addEventListener('click', function() { + const partnerId = this.getAttribute('data-partner-id'); + // Переход на страницу истории реализации + window.location.href = `/sales_history/${partnerId}`; + }); + }); +}); \ No newline at end of file diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..77fdb9f --- /dev/null +++ b/templates/about.html @@ -0,0 +1,18 @@ + + + + + About Flask + + + {% extends "template.html" %} + {% block content %} + +

About Flask

+

Flask is a micro web framework written in Python.

+

Applications that use the Flask framework include Pinterest, + LinkedIn, and the community web page for Flask itself.

+ + {% endblock %} + + \ No newline at end of file diff --git a/templates/add_client.html b/templates/add_client.html new file mode 100644 index 0000000..5a05fcb --- /dev/null +++ b/templates/add_client.html @@ -0,0 +1,521 @@ +{% extends "base.html" %} + +{% block title %}Добавление поставщика{% endblock %} + +{% block content %} +
+
+
+ + + +
+
+

+ Добавление нового поставщика +

+
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ + {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ +
+
+ Основная информация +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
От 0 до 5 (чем выше, тем надежнее)
+
+
+ + +
+
+ Контактные данные +
+ +
+ + +
+ +
+ + +
+ +
+ + +
10 или 12 цифр
+
+
+ + +
+
+ + +
+
+
+ + +
+ + Отмена + + +
+
+
+
+
+
+
+ + + +{% block scripts %} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/add_order.html b/templates/add_order.html new file mode 100644 index 0000000..399501d --- /dev/null +++ b/templates/add_order.html @@ -0,0 +1,737 @@ +{% extends "base.html" %} + +{% block title %}Добавление заказа{% endblock %} + +{% block content %} +
+
+
+ + + +
+
+

+ Добавление заказа +

+
+ +
+ +
+
+ Информация о поставщике +
+
+
+
+
+ Поставщик: + {{ client.name }} +
+
+ Тип: + {{ client.contractor_type_name }} +
+
+
+
+ Email: + {{ client.email }} +
+
+ Телефон: + {{ client.phone or 'Не указан' }} +
+
+
+
+
+ +
+
+
+
+ +
+ + +
+
+ +
+ + +
+ + Максимально доступно: 0 ед. +
+
+
+ +
+
+ + +
+ + + +
+
+ + +
+
+
+ + Материал: + Не выбран +
+
+ + На складе: + 0 ед. +
+
+
+ + +
+
+ Расчет стоимости +
+
+
+ Количество: + 1 ед. +
+
+ Цена за единицу: + 0 руб. +
+
+ Базовая сумма: + 0 руб. +
+
+ Транспортные расходы (10%): + 0 руб. +
+
+ + + Итоговая сумма : + + + 0.00 руб. + +
+
+
+ + +
+ + Отмена + + +
+
+
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/add_product.html b/templates/add_product.html new file mode 100644 index 0000000..cc88928 --- /dev/null +++ b/templates/add_product.html @@ -0,0 +1,458 @@ +{% extends "base.html" %} + +{% block title %}Добавление материала{% endblock %} + +{% block content %} +
+
+
+ + + +
+
+

+ Добавление нового материала +

+
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ + {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ +
+
+ Основная информация +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+ + +
+
+ Характеристики +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + Отмена + + +
+
+
+
+
+
+
+ + + +{% block scripts %} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..44a2977 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,686 @@ + + + + + + {% block title %}Строительные материалы{% endblock %} + + + + + + + + + + + + + + + + +
+ +
+
+
+ +

Стройматериалы

+
+ + + {% if request.endpoint in ['index', 'orders_list', 'catalog'] %} + + {% endif %} +
+
+ + +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %} + {% endblock %} +
+
+ + + + + + + + + + {% block scripts %} + {% endblock %} + + \ No newline at end of file diff --git a/templates/cart_new.html b/templates/cart_new.html new file mode 100644 index 0000000..659d6a5 --- /dev/null +++ b/templates/cart_new.html @@ -0,0 +1,731 @@ +{% extends "base.html" %} + +{% block title %}Моя корзина{% endblock %} + +{% block content %} +
+ + + +
+

+ Моя корзина +

+ + Продолжить покупки + +
+ + {% if cart_items %} + +
+
+ Транспортные расходы +
+
+

Транспортные расходы составляют 10% от стоимости материалов

+
+
+ + +
+ + + + + + + + + + + + + + + {% for item in cart_items %} + + + + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + +
МатериалПоставщикАртикулКатегорияЦенаКоличествоСуммаДействия
{{ item.product_name }}{{ item.supplier_name }}{{ item.article }}{{ item.category_name }}{{ item.price|int }} ₽ +
+ +
+
{{ (item.price * item.quantity)|int }} ₽ + +
Базовая сумма:{{ total_amount|int }} ₽
Транспортные расходы (10%):{{ (total_amount * 0.1)|int }} ₽
ИТОГО к оплате:{{ (total_amount * 1.1)|int }} ₽
+
+ + + {% if cart_items|groupby('supplier_id') %} +
+

Заказы по поставщикам

+
+ {% for supplier_id, items in cart_items|groupby('supplier_id') %} + {% set supplier_total = 0 %} + {% for item in items %} + {% set supplier_total = supplier_total + (item.price * item.quantity) %} + {% endfor %} + {% set supplier_total_with_transport = supplier_total * 1.1 %} + +
+
+ Поставщик: {{ items[0].supplier_name }} + {{ supplier_total_with_transport|int }} ₽ (с транспортными расходами) +
+
+ {% endfor %} +
+
+ {% endif %} + + +
+
+ +
+
+ + {% else %} + +
+
+ +
+

Корзина пуста

+

Добавьте материалы в корзину, чтобы оформить заказ

+ + Перейти в каталог + +
+ {% endif %} +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/catalog.html b/templates/catalog.html new file mode 100644 index 0000000..8c47253 --- /dev/null +++ b/templates/catalog.html @@ -0,0 +1,868 @@ +{% extends "base.html" %} + +{% block title %}Каталог материалов{% endblock %} + +{% block content %} + + + + +
+
+

+ Фильтры и поиск +

+ + Сбросить все + +
+ +
+ +
+
+ + + {% if request.args.get('search') %} + + + + {% endif %} +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ + + {% if request.args.get('search') or (request.args.get('category') and request.args.get('category') != 'all') or (request.args.get('supplier') and request.args.get('supplier') != 'all') %} +
+ Активные фильтры: +
+ {% if request.args.get('search') %} + + Поиск: "{{ request.args.get('search') }}" + + + + + {% endif %} + {% if request.args.get('category') and request.args.get('category') != 'all' %} + + {% for category in categories %} + {% if category.id == request.args.get('category')|int %} + Категория: {{ category.category_name }} + {% endif %} + {% endfor %} + + + + + {% endif %} + {% if request.args.get('supplier') and request.args.get('supplier') != 'all' %} + + {% for supplier in suppliers %} + {% if supplier.id == request.args.get('supplier')|int %} + Поставщик: {{ supplier.name }} + {% endif %} + {% endfor %} + + + + + {% endif %} +
+
+ {% endif %} + + + {% if products %} +
+ {% for product in products %} +
+
+ +
+
+ + {{ product.category_name }} + + | + {{ product.product_name }} +
+
+ + {% if product.quantity_in_stock and product.quantity_in_stock > 0 %} + {{ product.quantity_in_stock }} ед. + {% else %} + Нет в наличии + {% endif %} + +
+
+ + +
+ Артикул: + {{ product.article }} +
+ +
+ Материал: + + {% if product.material_name %} + {{ product.material_name }} + {% else %} + Не указан + {% endif %} + +
+ +
+ Поставщик: + + {% if product.supplier_name %} + {{ product.supplier_name }} + {% else %} + Не указан + {% endif %} + +
+ + {% if product.description %} +
+ Описание: + {{ product.description }} +
+ {% endif %} + +
+ Цена: + {{ product.min_price|int }} руб. +
+ + +
+ + Редактирование + + + +
+
+ + + +
+
+
+
+
+ {% endfor %} +
+ + +
+
+

+ + Найдено материалов: {{ products|length }} + {% if request.args.get('search') %} + по запросу "{{ request.args.get('search') }}" + {% endif %} + {% if request.args.get('category') and request.args.get('category') != 'all' %} + {% for category in categories %} + {% if category.id == request.args.get('category')|int %} + , категория: {{ category.category_name }} + {% endif %} + {% endfor %} + {% endif %} + {% if request.args.get('supplier') and request.args.get('supplier') != 'all' %} + {% for supplier in suppliers %} + {% if supplier.id == request.args.get('supplier')|int %} + , поставщик: {{ supplier.name }} + {% endif %} + {% endfor %} + {% endif %} +

+
+
+ + {% else %} +
+ + {% if request.args.get('search') or request.args.get('category') != 'all' or request.args.get('supplier') != 'all' %} + Ничего не найдено по вашему запросу. +
Сбросить все фильтры + {% else %} + В каталоге нет материалов. Добавьте первый материал. + {% endif %} +
+ {% endif %} + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/components/header.html b/templates/components/header.html new file mode 100644 index 0000000..a80b901 --- /dev/null +++ b/templates/components/header.html @@ -0,0 +1,8 @@ +
+
+
+ +

Строительные материалы

+
+
+
\ No newline at end of file diff --git a/templates/components/partner_card.html b/templates/components/partner_card.html new file mode 100644 index 0000000..5eeccc1 --- /dev/null +++ b/templates/components/partner_card.html @@ -0,0 +1,249 @@ +
+
+ +
+
+ {% if partner.contractor_type_name %} + {{ partner.contractor_type_name }} + {% else %} + Тип не указан + {% endif %} +
+
{{ partner.name }}
+ +
+ + +
+
+
+ Директор: + {{ partner.director or 'Не указан' }} +
+
+ Телефон: + {{ partner.phone or 'Не указан' }} +
+
+
+
+ Рейтинг: + + + {{ partner.rating }} + + +
+
+ Сумма заказов: + {{ "%.2f"|format(partner.total_sales) }} руб. +
+
+
+ + + +
+
+ + \ No newline at end of file diff --git a/templates/edit_client.html b/templates/edit_client.html new file mode 100644 index 0000000..ae18c30 --- /dev/null +++ b/templates/edit_client.html @@ -0,0 +1,815 @@ +{% extends "base.html" %} + +{% block title %}Редактирование поставщика{% endblock %} + +{% block content %} +
+
+
+ + + +
+
+

+ Редактирование поставщика +

+
+ +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ + {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ +
+
+ Основная информация +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
От 0.0 до 5.0 (чем выше, тем надежнее)
+
+
+ + +
+
+ Контактные данные +
+ +
+ + +
+ +
+ +
+ + +
+
+ Формат: +7 (XXX) XXX-XX-XX +
+ +
+ +
+ + +
10 или 12 цифр
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ + История заказов + + +
+
+
+
+
+
+
+
+ + + +{% block scripts %} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/edit_product.html b/templates/edit_product.html new file mode 100644 index 0000000..abd7f68 --- /dev/null +++ b/templates/edit_product.html @@ -0,0 +1,454 @@ +{% extends "base.html" %} + +{% block title %}Редактирование материала{% endblock %} + +{% block content %} +
+
+
+ + + +
+
+

+ Редактирование материала +

+
+ +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ + {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ +
+
+ Основная информация +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+ + +
+
+ Характеристики +
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + Отмена + + +
+
+
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..085f57c --- /dev/null +++ b/templates/home.html @@ -0,0 +1,16 @@ + + + + + Flask Tutorial + + + {% extends "template.html" %} + {% block content %} + +

My First Try Using Flask

+

Flask is Fun

+ + {% endblock %} + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..376a9ba --- /dev/null +++ b/templates/index.html @@ -0,0 +1,547 @@ +{% extends "base.html" %} + +{% block title %}Поставщики строительных материалов{% endblock %} + +{% block content %} + +
+

Поставщики материалов

+ + Новый поставщик + +
+ + +
+
+

+ Поиск и сортировка +

+ + Сбросить все + +
+ +
+ +
+
+ + + {% if request.args.get('search') %} + + + + {% endif %} +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ + + {% if request.args.get('search') or (request.args.get('type') and request.args.get('type') != 'all') or (request.args.get('rating') and request.args.get('rating') != '0') %} +
+ Активные фильтры: +
+ {% if request.args.get('search') %} + + Поиск: "{{ request.args.get('search') }}" + + + + + {% endif %} + {% if request.args.get('type') and request.args.get('type') != 'all' %} + + Тип: {{ request.args.get('type') }} + + + + + {% endif %} + {% if request.args.get('rating') and request.args.get('rating') != '0' %} + + Рейтинг ≥ {{ request.args.get('rating') }} + + + + + {% endif %} +
+
+ {% endif %} + + + {% if partners %} +
+ {% for partner in partners %} + {% include 'components/partner_card.html' %} + {% endfor %} +
+ + +
+
+

+ + Найдено поставщиков: {{ partners|length }} + {% if request.args.get('search') %} + по запросу "{{ request.args.get('search') }}" + {% endif %} + {% if request.args.get('type') and request.args.get('type') != 'all' %} + , тип: {{ request.args.get('type') }} + {% endif %} + {% if request.args.get('rating') and request.args.get('rating') != '0' %} + , рейтинг ≥ {{ request.args.get('rating') }} + {% endif %} +

+
+
+ {% else %} +
+ + {% if request.args.get('search') or request.args.get('type') != 'all' or request.args.get('rating') != '0' %} + Ничего не найдено по вашему запросу. +
Сбросить все фильтры + {% else %} + Нет данных о поставщиках. Добавьте первого поставщика. + {% endif %} +
+ {% endif %} + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/materials.html b/templates/materials.html new file mode 100644 index 0000000..f959b22 --- /dev/null +++ b/templates/materials.html @@ -0,0 +1,266 @@ +{% extends "base.html" %} + +{% block title %}Материалы{% endblock %} + +{% block content %} + +
+

Материалы

+ + Всего: {{ materials|length }} + +
+ + + {% if materials %} +
+ {% for material in materials %} +
+
+
+
+ {{ material.material_name }} +
+
+
+ {% if material.description %} +

{{ material.description }}

+ {% endif %} + +
+ {% if material.category %} +
+ Категория: + {{ material.category }} +
+ {% endif %} + + {% if material.color %} +
+ Цвет: + {{ material.color }} +
+ {% endif %} + + {% if material.density %} +
+ Плотность: + {{ material.density }} кг/м³ +
+ {% endif %} + + {% if material.thickness %} +
+ Толщина: + {{ material.thickness }} мм +
+ {% endif %} + + {% if material.supplier %} +
+ Поставщик: + {{ material.supplier }} +
+ {% endif %} + + {% if material.country %} +
+ Страна: + {{ material.country }} +
+ {% endif %} + + {% if material.wear_class %} +
+ Класс износа: + {{ material.wear_class }} +
+ {% endif %} +
+ +
+ {% if material.price_per_unit %} +
+ Цена: + {{ material.price_per_unit|int }} руб./{{ material.unit or 'шт' }} +
+ {% endif %} + +
+ В наличии: + + {{ material.in_stock or 0 }} {{ material.unit or 'шт' }} + +
+
+ + +
+
+
+ {% endfor %} +
+ {% else %} +
+ +

Нет материалов

+

В базе данных пока нет материалов. Добавьте материалы через импорт данных.

+
+ {% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/order_history.html b/templates/order_history.html new file mode 100644 index 0000000..ef69ff5 --- /dev/null +++ b/templates/order_history.html @@ -0,0 +1,522 @@ +{% extends "base.html" %} + +{% block title %}История заказов - {{ client.name }}{% endblock %} + +{% block content %} +
+ + + + +
+

История заказов - {{ client.name }}

+
+ {% if client.rating %} + Рейтинг: {{ client.rating }} + {% endif %} + {% if total_stats %} + Всего заказов: {{ total_stats.total_orders }} + Выполнено: {{ total_stats.completed_orders }} + Общая сумма: {{ "%.2f"|format(total_stats.total_amount or 0) }} руб. + {% endif %} +
+
+ + + + + +
+ + + + + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
ID заказаДатаТоварыБазовая стоимостьТранспортные расходы (10%)СкидкаИтоговая стоимостьСтатусДействия
{{ order.id }}{{ order.order_date }} + + {{ "%.2f"|format(order.base_amount) }} руб.{{ "%.2f"|format(order.shipping_cost) }} руб. + {% if order.discount and order.discount > 0 %} + {{ order.discount }}% + {% else %} + - + {% endif %} + {{ "%.2f"|format(order.display_total) }} руб. + + {{ order.status }} + + +
+ +
+
+

У этого клиента пока нет заказов

+ + Создать первый заказ + +
+
+ + +
+ + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/orders_list.html b/templates/orders_list.html new file mode 100644 index 0000000..d46360d --- /dev/null +++ b/templates/orders_list.html @@ -0,0 +1,410 @@ +{% extends "base.html" %} + +{% block title %}Список заказов{% endblock %} + +{% block content %} + +
+

Список заказов

+
+ + +
+
+ + +
+
+
+
+
Всего заказов
+

{{ total_stats.total_orders or 0 }}

+
+
+
+
+
+
+
Общая сумма
+

{{ (total_stats.total_amount or 0)|int }} руб.

+
+
+
+
+ + + {% if orders %} +
+ + + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + + {% endfor %} + +
IDДатаПоставщикИННТоварыСуммаСтатус
{{ order.id }}{{ order.order_date }} + + {{ order.client_name }} + + {{ order.client_inn or '-' }} + {% if order.product_name %} + + {{ order.product_name }} + {% elif order.products_list %} + + {{ order.products_list|truncate(50) }} + {% else %} + — + {% endif %} + {{ order.total_amount|int }} руб. + + {{ order.status }} + +
+
+ {% else %} +
+ + Нет данных о заказах. Вернуться к поставщикам. +
+ {% endif %} + + + + + + +{% endblock %} \ No newline at end of file