diff --git a/app.py b/app.py new file mode 100644 index 0000000..e95efb8 --- /dev/null +++ b/app.py @@ -0,0 +1,722 @@ +from flask import Flask, render_template, request, redirect, url_for, flash, jsonify +import sqlite3 +import os +import pandas as pd +from datetime import datetime +from database import get_db_connection, calculate_order_total, calculate_discount, get_discount_description, \ + calculate_discount_for_new_order, get_db, create_tables +from import_data import import_data_from_files + +app = Flask(__name__) +app.secret_key = 'kancelyary_secret_key_2026' +app.config['TEMPLATES_AUTO_RELOAD'] = True +app.config['DATABASE'] = 'partners.db' + + +# Функция для сохранения данных в Excel (опционально) +def save_to_excel(): + """Сохраняет все данные из БД в Excel файлы""" + try: + if not os.path.exists('data'): + os.makedirs('data') + + with get_db() as conn: + product_types = conn.execute('SELECT * FROM product_types').fetchall() + if product_types: + df_types = pd.DataFrame([dict(pt) for pt in product_types]) + df_types.to_excel('data/Product_types_import.xlsx', index=False) + + partners = conn.execute('SELECT * FROM partners').fetchall() + if partners: + df_partners = pd.DataFrame([dict(p) for p in partners]) + df_partners.to_excel('data/Partners_import.xlsx', index=False) + + products = conn.execute('SELECT * FROM products').fetchall() + if products: + df_products = pd.DataFrame([dict(p) for p in products]) + df_products.to_excel('data/Products_import.xlsx', index=False) + + orders = conn.execute('SELECT * FROM orders').fetchall() + if orders: + df_orders = pd.DataFrame([dict(o) for o in orders]) + df_orders.to_excel('data/Orders_import.xlsx', index=False) + + order_items = conn.execute('SELECT * FROM order_items').fetchall() + if order_items: + df_items = pd.DataFrame([dict(oi) for oi in order_items]) + df_items.to_excel('data/Order_items_import.xlsx', index=False) + + print("✅ Данные сохранены в Excel файлы") + return True + except Exception as e: + print(f"❌ Ошибка при сохранении в Excel: {e}") + return False + + +# Главная страница - список заказов с сортировкой +@app.route('/') +def index(): + sort_by = request.args.get('sort', 'date_desc') + partner_filter = request.args.get('partner', None) + + try: + with get_db() as conn: + if partner_filter: + query = f''' + SELECT o.*, p.name as partner_name, p.id as partner_id + FROM orders o + JOIN partners p ON o.partner_id = p.id + WHERE o.partner_id = {partner_filter} + ''' + else: + query = ''' + SELECT o.*, p.name as partner_name, p.id as partner_id + FROM orders o + JOIN partners p ON o.partner_id = p.id + ''' + + if sort_by == 'amount_desc': + query += ''' + LEFT JOIN order_items oi ON o.id = oi.order_id + GROUP BY o.id + ORDER BY COALESCE(SUM(oi.quantity * oi.price_at_order), 0) * 1.1 DESC + ''' + elif sort_by == 'date_desc': + query += ' ORDER BY o.order_date DESC' + elif sort_by == 'partner': + query += ' ORDER BY p.name, o.order_date DESC' + else: + query += ' ORDER BY o.order_date DESC' + + orders = conn.execute(query).fetchall() + + orders_with_discount = [] + for order in orders: + try: + items_count = conn.execute(''' + SELECT COUNT(*) as count FROM order_items WHERE order_id = ? + ''', (order['id'],)).fetchone()['count'] + + subtotal_without_markup = conn.execute(''' + SELECT COALESCE(SUM(quantity * price_at_order), 0) as total + FROM order_items WHERE order_id = ? + ''', (order['id'],)).fetchone()['total'] + + subtotal_with_markup = subtotal_without_markup * 1.1 + discount_percent = order['discount_percent'] if order['discount_percent'] else 0 + discount_amount = subtotal_with_markup * discount_percent / 100 + total = subtotal_with_markup - discount_amount + + order_dict = { + 'order_id': order['id'], + 'partner_id': order['partner_id'], + 'partner_name': order['partner_name'], + 'order_date': order['order_date'], + 'notes': order['notes'] if order['notes'] else '', + 'items_count': items_count, + 'subtotal_without_markup': round(subtotal_without_markup, 2), + 'subtotal_with_markup': round(subtotal_with_markup, 2), + 'discount_percent': discount_percent, + 'discount_desc': get_discount_description(discount_percent), + 'discount_amount': round(discount_amount, 2), + 'total': round(total, 2) + } + + orders_with_discount.append(order_dict) + + except Exception as e: + print(f"Ошибка при обработке заказа {order['id']}: {e}") + orders_with_discount.append({ + 'order_id': order['id'], + 'partner_id': order['partner_id'], + 'partner_name': order['partner_name'], + 'order_date': order['order_date'], + 'notes': '', + 'items_count': 0, + 'subtotal_without_markup': 0, + 'subtotal_with_markup': 0, + 'discount_percent': 0, + 'discount_desc': '0%', + 'discount_amount': 0, + 'total': 0 + }) + + return render_template('index.html', orders=orders_with_discount, current_sort=sort_by) + + except Exception as e: + print(f"Критическая ошибка в index: {e}") + return render_template('index.html', orders=[], current_sort='date_desc') + + +# Страница товаров +@app.route('/products') +def products(): + try: + with get_db() as conn: + products_rows = conn.execute(''' + SELECT p.*, pt.type_name as product_type_name + FROM products p + JOIN product_types pt ON p.product_type_id = pt.id + ORDER BY p.product_name + ''').fetchall() + + products_list = [] + for row in products_rows: + products_list.append({ + 'id': row['id'], + 'product_type_id': row['product_type_id'], + 'product_name': row['product_name'], + 'article': row['article'], + 'min_partner_price': row['min_partner_price'], + 'product_type_name': row['product_type_name'] + }) + + product_types_rows = conn.execute('SELECT * FROM product_types ORDER BY type_name').fetchall() + product_types = [] + for row in product_types_rows: + product_types.append({ + 'id': row['id'], + 'type_name': row['type_name'], + 'description': row['description'] + }) + + partners_rows = conn.execute('SELECT id, name FROM partners ORDER BY name').fetchall() + partners = [] + for row in partners_rows: + partners.append({ + 'id': row['id'], + 'name': row['name'] + }) + + today = datetime.now().strftime('%Y-%m-%d') + + return render_template('products.html', products=products_list, product_types=product_types, + partners=partners, today=today) + except Exception as e: + print(f"Ошибка на странице товаров: {e}") + flash('Ошибка при загрузке страницы', 'error') + return redirect(url_for('index')) + + +# Страница партнеров +@app.route('/partners') +def partners(): + try: + with get_db() as conn: + partners_rows = conn.execute(''' + SELECT p.*, + COUNT(DISTINCT o.id) as orders_count, + COALESCE(SUM(oi.quantity * oi.price_at_order), 0) as total_purchases + FROM partners p + LEFT JOIN orders o ON p.id = o.partner_id + LEFT JOIN order_items oi ON o.id = oi.order_id + GROUP BY p.id + ORDER BY p.name + ''').fetchall() + + partners_with_stats = [] + for partner in partners_rows: + partner_dict = { + 'id': partner['id'], + 'name': partner['name'], + 'director': partner['director'], + 'email': partner['email'], + 'phone': partner['phone'], + 'legal_address': partner['legal_address'], + 'inn': partner['inn'], + 'orders_count': partner['orders_count'], + 'total_purchases': partner['total_purchases'] + } + discount_percent = calculate_discount(partner['total_purchases'] or 0) + discount_desc = get_discount_description(discount_percent) + partner_dict['discount_percent'] = discount_percent + partner_dict['discount_desc'] = discount_desc + partners_with_stats.append(partner_dict) + + return render_template('partners.html', partners=partners_with_stats) + except Exception as e: + print(f"Ошибка на странице партнеров: {e}") + import traceback + traceback.print_exc() + flash('Ошибка при загрузке страницы', 'error') + return redirect(url_for('index')) + + +# Добавление заказа +@app.route('/add_order', methods=['GET', 'POST']) +def add_order(): + if request.method == 'POST': + try: + partner_id = request.form['partner_id'] + order_date = request.form['order_date'] + notes = request.form.get('notes', '') + + products = request.form.getlist('product_id[]') + quantities = request.form.getlist('quantity[]') + prices = request.form.getlist('price[]') + + with get_db() as conn: + # Проверка (товары всегда есть в наличии, но проверяем что они существуют) + for i in range(len(products)): + if products[i] and quantities[i]: + product_id = int(products[i]) + quantity = int(quantities[i]) + + if quantity <= 0: + flash(f'Количество должно быть положительным для позиции {i + 1}', 'warning') + return redirect(url_for('add_order')) + + product = conn.execute('SELECT * FROM products WHERE id = ?', + (product_id,)).fetchone() + if not product: + flash(f'Товар с ID {product_id} не найден', 'error') + return redirect(url_for('add_order')) + + # Рассчитываем сумму текущего заказа (без добавочной стоимости) + current_order_amount_without_markup = 0 + for i in range(len(products)): + if products[i] and quantities[i] and prices[i]: + quantity = int(quantities[i]) + price = float(prices[i]) + current_order_amount_without_markup += quantity * price + + # Рассчитываем скидку на основе предыдущих заказов + discount_percent = calculate_discount_for_new_order(partner_id, order_date, + current_order_amount_without_markup) + + # Создаем заказ + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO orders (partner_id, order_date, notes, discount_percent) + VALUES (?, ?, ?, ?) + ''', (partner_id, order_date, notes, discount_percent)) + + order_id = cursor.lastrowid + + # Добавляем позиции + items_added = 0 + for i in range(len(products)): + if products[i] and quantities[i] and prices[i]: + product_id = int(products[i]) + quantity = int(quantities[i]) + price = float(prices[i]) + + cursor.execute(''' + INSERT INTO order_items (order_id, product_id, quantity, price_at_order) + VALUES (?, ?, ?, ?) + ''', (order_id, product_id, quantity, price)) + + items_added += 1 + + save_to_excel() + + if items_added > 0: + flash(f'Заказ успешно создан! Добавлено {items_added} позиций', 'success') + + return redirect(url_for('index')) + + except Exception as e: + print(f"Ошибка при создании заказа: {e}") + flash(f'Ошибка при создании заказа: {str(e)}', 'error') + return redirect(url_for('add_order')) + + # GET запрос + try: + with get_db() as conn: + partners_rows = conn.execute('SELECT * FROM partners ORDER BY name').fetchall() + partners = [] + for row in partners_rows: + partners.append({ + 'id': row['id'], + 'name': row['name'], + 'director': row['director'], + 'email': row['email'], + 'phone': row['phone'], + 'legal_address': row['legal_address'], + 'inn': row['inn'] + }) + + products_rows = conn.execute(''' + SELECT p.*, pt.type_name + FROM products p + JOIN product_types pt ON p.product_type_id = pt.id + ORDER BY p.product_name + ''').fetchall() + + products = [] + for row in products_rows: + products.append({ + 'id': row['id'], + 'product_type_id': row['product_type_id'], + 'product_name': row['product_name'], + 'article': row['article'], + 'min_partner_price': row['min_partner_price'], + 'type_name': row['type_name'] + }) + + today = datetime.now().strftime('%Y-%m-%d') + + return render_template('add_order.html', partners=partners, products=products, today=today) + except Exception as e: + print(f"Ошибка при загрузке формы: {e}") + flash('Ошибка при загрузке формы', 'error') + return redirect(url_for('index')) + + +# Редактирование заказа (УБРАН МАРШРУТ УДАЛЕНИЯ) +@app.route('/edit_order/', methods=['GET', 'POST']) +def edit_order(order_id): + if request.method == 'POST': + try: + order_date = request.form['order_date'] + notes = request.form.get('notes', '') + + with get_db() as conn: + old_items = conn.execute('SELECT product_id, quantity FROM order_items WHERE order_id = ?', + (order_id,)).fetchall() + + products = request.form.getlist('product_id[]') + quantities = request.form.getlist('quantity[]') + prices = request.form.getlist('price[]') + + has_new_items = any(p and p.strip() for p in products) + + if has_new_items: + conn.execute('DELETE FROM order_items WHERE order_id = ?', (order_id,)) + + items_added = 0 + new_order_amount_without_markup = 0 + + for i in range(len(products)): + if products[i] and products[i].strip(): + product_id = int(products[i]) + quantity = int(quantities[i]) if quantities[i] and quantities[i].strip() else 1 + price = float(prices[i]) if prices[i] and prices[i].strip() else 0 + + conn.execute(''' + INSERT INTO order_items (order_id, product_id, quantity, price_at_order) + VALUES (?, ?, ?, ?) + ''', (order_id, product_id, quantity, price)) + + new_order_amount_without_markup += quantity * price + items_added += 1 + + order_info = conn.execute('SELECT partner_id FROM orders WHERE id = ?', (order_id,)).fetchone() + partner_id = order_info['partner_id'] + + discount_percent = calculate_discount_for_new_order(partner_id, order_date, + new_order_amount_without_markup, order_id) + + conn.execute(''' + UPDATE orders + SET order_date = ?, notes = ?, discount_percent = ? + WHERE id = ? + ''', (order_date, notes, discount_percent, order_id)) + else: + items_added = len(old_items) + conn.execute(''' + UPDATE orders + SET order_date = ?, notes = ? + WHERE id = ? + ''', (order_date, notes, order_id)) + + save_to_excel() + + flash(f'Заказ обновлен! {items_added} позиций', 'success') + return redirect(url_for('index')) + + except Exception as e: + print(f"Ошибка при редактировании заказа: {e}") + import traceback + traceback.print_exc() + flash(f'Ошибка: {str(e)}', 'error') + return redirect(url_for('edit_order', order_id=order_id)) + + try: + with get_db() as conn: + order_row = conn.execute('SELECT * FROM orders WHERE id = ?', (order_id,)).fetchone() + if not order_row: + return redirect(url_for('index')) + + order = { + 'id': order_row['id'], + 'partner_id': order_row['partner_id'], + 'order_date': order_row['order_date'], + 'notes': order_row['notes'] + } + + items_rows = conn.execute(''' + SELECT oi.*, p.product_name, p.article + FROM order_items oi + JOIN products p ON oi.product_id = p.id + WHERE oi.order_id = ? + ''', (order_id,)).fetchall() + + items = [] + for row in items_rows: + items.append({ + 'id': row['id'], + 'order_id': row['order_id'], + 'product_id': row['product_id'], + 'quantity': row['quantity'], + 'price_at_order': row['price_at_order'], + 'product_name': row['product_name'], + 'article': row['article'] + }) + + products_rows = conn.execute(''' + SELECT p.*, pt.type_name + FROM products p + JOIN product_types pt ON p.product_type_id = pt.id + ORDER BY p.product_name + ''').fetchall() + + products = [] + for row in products_rows: + products.append({ + 'id': row['id'], + 'product_type_id': row['product_type_id'], + 'product_name': row['product_name'], + 'article': row['article'], + 'min_partner_price': row['min_partner_price'], + 'type_name': row['type_name'] + }) + + return render_template('edit_order.html', order=order, items=items, products=products) + except Exception as e: + print(f"Ошибка при загрузке заказа: {e}") + flash('Ошибка при загрузке заказа', 'error') + return redirect(url_for('index')) + + +# Добавление партнера +@app.route('/add_partner', methods=['GET', 'POST']) +def add_partner(): + if request.method == 'POST': + try: + name = request.form['name'] + director = request.form.get('director', '') + email = request.form['email'] + phone = request.form.get('phone', '') + legal_address = request.form.get('legal_address', '') + inn = request.form.get('inn', '') + + with get_db() as conn: + conn.execute(''' + INSERT INTO partners (name, director, email, phone, legal_address, inn) + VALUES (?, ?, ?, ?, ?, ?) + ''', (name, director, email, phone, legal_address, inn)) + + save_to_excel() + + flash('Партнер успешно добавлен!', 'success') + return redirect(url_for('partners')) + + except sqlite3.IntegrityError: + flash('Партнер с таким ИНН уже существует', 'error') + return redirect(url_for('add_partner')) + except Exception as e: + print(f"Ошибка при добавлении партнера: {e}") + flash(f'Ошибка: {str(e)}', 'error') + return redirect(url_for('add_partner')) + + return render_template('add_partner.html') + + +# Редактирование партнера (УБРАН МАРШРУТ УДАЛЕНИЯ) +@app.route('/edit_partner/', methods=['GET', 'POST']) +def edit_partner(partner_id): + if request.method == 'POST': + try: + name = request.form['name'] + director = request.form.get('director', '') + email = request.form['email'] + phone = request.form.get('phone', '') + legal_address = request.form.get('legal_address', '') + inn = request.form.get('inn', '') + + with get_db() as conn: + existing = conn.execute( + 'SELECT id FROM partners WHERE inn = ? AND id != ?', + (inn, partner_id) + ).fetchone() + + if existing: + flash('Партнер с таким ИНН уже существует', 'error') + return redirect(url_for('edit_partner', partner_id=partner_id)) + + conn.execute(''' + UPDATE partners + SET name = ?, director = ?, email = ?, phone = ?, + legal_address = ?, inn = ? + WHERE id = ? + ''', (name, director, email, phone, legal_address, inn, partner_id)) + + save_to_excel() + + flash('Данные партнера обновлены!', 'success') + return redirect(url_for('partners')) + + except Exception as e: + print(f"Ошибка при редактировании партнера: {e}") + flash(f'Ошибка: {str(e)}', 'error') + return redirect(url_for('edit_partner', partner_id=partner_id)) + + try: + with get_db() as conn: + partner_row = conn.execute('SELECT * FROM partners WHERE id = ?', (partner_id,)).fetchone() + + if not partner_row: + flash('Партнер не найден', 'error') + return redirect(url_for('partners')) + + partner = { + 'id': partner_row['id'], + 'name': partner_row['name'], + 'director': partner_row['director'], + 'email': partner_row['email'], + 'phone': partner_row['phone'], + 'legal_address': partner_row['legal_address'], + 'inn': partner_row['inn'] + } + + return render_template('edit_partner.html', partner=partner) + except Exception as e: + print(f"Ошибка при загрузке партнера: {e}") + flash('Ошибка при загрузке данных', 'error') + return redirect(url_for('partners')) + + +# API для работы с товарами (УБРАН МАРШРУТ УДАЛЕНИЯ ТОВАРА) +@app.route('/api/product/', methods=['GET']) +def api_get_product(product_id): + try: + with get_db() as conn: + product = conn.execute('SELECT * FROM products WHERE id = ?', (product_id,)).fetchone() + if product: + return jsonify(dict(product)) + return jsonify({'error': 'Product not found'}), 404 + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/product', methods=['POST']) +def api_add_product(): + data = request.json + + try: + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO products (product_type_id, product_name, article, min_partner_price) + VALUES (?, ?, ?, ?) + ''', ( + data['product_type_id'], + data['product_name'], + data['article'], + data['min_partner_price'] + )) + + product_id = cursor.lastrowid + save_to_excel() + + return jsonify({'success': True, 'product_id': product_id}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 400 + + +@app.route('/api/product/', methods=['PUT']) +def api_update_product(product_id): + data = request.json + + try: + with get_db() as conn: + conn.execute(''' + UPDATE products + SET product_type_id = ?, product_name = ?, article = ?, + min_partner_price = ? + WHERE id = ? + ''', ( + data['product_type_id'], + data['product_name'], + data['article'], + data['min_partner_price'], + product_id + )) + + save_to_excel() + + return jsonify({'success': True}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 400 + + +@app.route('/api/product_type', methods=['POST']) +def api_add_product_type(): + data = request.json + + try: + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO product_types (type_name, description) + VALUES (?, ?) + ''', ( + data['type_name'], + data.get('description', '') + )) + + type_id = cursor.lastrowid + save_to_excel() + + return jsonify({'success': True, 'type_id': type_id}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 400 + + +@app.route('/api/order', methods=['POST']) +def api_create_order(): + data = request.json + + try: + with get_db() as conn: + for item in data['items']: + product = conn.execute('SELECT * FROM products WHERE id = ?', + (item['product_id'],)).fetchone() + if not product: + return jsonify({'success': False, 'error': 'Товар не найден'}), 400 + + order_amount_without_markup = sum(item['quantity'] * item['price'] for item in data['items']) + + discount_percent = calculate_discount_for_new_order(data['partner_id'], data['order_date'], + order_amount_without_markup) + + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO orders (partner_id, order_date, notes, discount_percent) + VALUES (?, ?, ?, ?) + ''', (data['partner_id'], data['order_date'], data.get('notes', ''), discount_percent)) + + order_id = cursor.lastrowid + + for item in data['items']: + cursor.execute(''' + INSERT INTO order_items (order_id, product_id, quantity, price_at_order) + VALUES (?, ?, ?, ?) + ''', (order_id, item['product_id'], item['quantity'], item['price'])) + + save_to_excel() + + return jsonify({'success': True, 'order_id': order_id}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 400 + +@app.route('/test_partner') +def test_partner(): + """Страница для ручного тестирования добавления партнера""" + return render_template('manual_partner_test.html') + +if __name__ == '__main__': + if not os.path.exists('data'): + os.makedirs('data') + app.run(debug=True) \ No newline at end of file diff --git a/data/Order_items_import.xlsx b/data/Order_items_import.xlsx new file mode 100644 index 0000000..d0c315d Binary files /dev/null and b/data/Order_items_import.xlsx differ diff --git a/data/Orders_import.xlsx b/data/Orders_import.xlsx new file mode 100644 index 0000000..ded8059 Binary files /dev/null and b/data/Orders_import.xlsx differ diff --git a/data/Partners_import.xlsx b/data/Partners_import.xlsx new file mode 100644 index 0000000..68a0a0e Binary files /dev/null and b/data/Partners_import.xlsx differ diff --git a/data/Product_types_import.xlsx b/data/Product_types_import.xlsx new file mode 100644 index 0000000..311933b Binary files /dev/null and b/data/Product_types_import.xlsx differ diff --git a/data/Products_import.xlsx b/data/Products_import.xlsx new file mode 100644 index 0000000..9c0919d Binary files /dev/null and b/data/Products_import.xlsx differ diff --git a/database.py b/database.py new file mode 100644 index 0000000..8365730 --- /dev/null +++ b/database.py @@ -0,0 +1,221 @@ +import sqlite3 +import time +from contextlib import contextmanager +from datetime import datetime + +DATABASE = 'partners.db' + + +def get_db_connection(): + """Создает подключение к базе данных с правильными настройками""" + conn = sqlite3.connect(DATABASE, timeout=10) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + conn.execute("PRAGMA journal_mode = WAL") + return conn + + +@contextmanager +def get_db(): + """Контекстный менеджер для безопасной работы с БД""" + conn = None + try: + conn = get_db_connection() + yield conn + if conn: + conn.commit() + except Exception as e: + if conn: + conn.rollback() + raise e + finally: + if conn: + conn.close() + + +def create_tables(): + """Создает все таблицы в базе данных""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.executescript(''' + DROP TABLE IF EXISTS order_items; + DROP TABLE IF EXISTS orders; + DROP TABLE IF EXISTS products; + DROP TABLE IF EXISTS partners; + DROP TABLE IF EXISTS product_types; + + CREATE TABLE product_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type_name VARCHAR(100) NOT NULL UNIQUE, + description TEXT + ); + + CREATE TABLE partners ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + director VARCHAR(255), + email VARCHAR(255) NOT NULL, + phone VARCHAR(50), + legal_address TEXT, + inn VARCHAR(20) UNIQUE + ); + + CREATE TABLE products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_type_id INTEGER NOT NULL, + product_name VARCHAR(255) NOT NULL, + article VARCHAR(100) UNIQUE NOT NULL, + min_partner_price DECIMAL(15,2) NOT NULL CHECK (min_partner_price > 0), + FOREIGN KEY (product_type_id) REFERENCES product_types(id) ON DELETE RESTRICT + ); + + CREATE TABLE orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + partner_id INTEGER NOT NULL, + order_date DATE NOT NULL, + notes TEXT, + discount_percent INTEGER DEFAULT 0, + FOREIGN KEY (partner_id) REFERENCES partners(id) ON DELETE RESTRICT + ); + + CREATE TABLE order_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + price_at_order DECIMAL(15,2) NOT NULL CHECK (price_at_order > 0), + FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT + ); + ''') + + print("✅ Таблицы успешно созданы") + + +def calculate_discount(total_amount): + """Рассчитывает скидку на основе общей суммы закупок за год""" + if total_amount >= 300000: + return 50 + elif total_amount >= 200000: + return 30 + elif total_amount >= 150000: + return 15 + else: + return 0 + + +def get_discount_description(discount_percent): + """Возвращает описание скидки""" + if discount_percent == 50: + return '50% (300 000+ руб.)' + elif discount_percent == 30: + return '30% (200 000 - 299 999 руб.)' + elif discount_percent == 15: + return '15% (150 000 - 199 999 руб.)' + else: + return '0% (до 150 000 руб.)' + + +def calculate_order_total(order_id): + """Рассчитывает общую стоимость заказа с учетом скидки и добавочной стоимости 10%""" + try: + with get_db() as conn: + order_info = conn.execute(''' + SELECT o.*, p.id as partner_id, p.name as partner_name + FROM orders o + JOIN partners p ON o.partner_id = p.id + WHERE o.id = ? + ''', (order_id,)).fetchone() + + if not order_info: + return None + + items = conn.execute(''' + SELECT oi.*, p.product_name + FROM order_items oi + JOIN products p ON oi.product_id = p.id + WHERE oi.order_id = ? + ''', (order_id,)).fetchall() + + subtotal_without_markup = 0 + items_list = [] + + for item in items: + # Базовая стоимость = количество × цена + base_item_subtotal = item['quantity'] * item['price_at_order'] + # Добавляем 10% добавочной стоимости + item_subtotal = base_item_subtotal * 1.1 + subtotal_without_markup += base_item_subtotal + items_list.append({ + 'id': item['id'], + 'product_name': item['product_name'], + 'quantity': item['quantity'], + 'price': item['price_at_order'], + 'base_subtotal': round(base_item_subtotal, 2), + 'subtotal_with_markup': round(item_subtotal, 2) + }) + + # Общая стоимость с добавочной стоимостью (без скидки) + subtotal_with_markup = subtotal_without_markup * 1.1 + + discount_percent = order_info['discount_percent'] if order_info['discount_percent'] else 0 + discount_desc = get_discount_description(discount_percent) + + discount_amount = subtotal_with_markup * discount_percent / 100 + total = subtotal_with_markup - discount_amount + + return { + 'order_id': order_id, + 'partner_id': order_info['partner_id'], + 'partner_name': order_info['partner_name'], + 'order_date': order_info['order_date'], + 'notes': order_info['notes'] if order_info['notes'] else '', + 'items': items_list, + 'items_count': len(items_list), + 'subtotal_without_markup': round(subtotal_without_markup, 2), + 'subtotal_with_markup': round(subtotal_with_markup, 2), + 'discount_percent': discount_percent, + 'discount_desc': discount_desc, + 'discount_amount': round(discount_amount, 2), + 'total': round(total, 2) + } + except Exception as e: + print(f"Ошибка при расчете стоимости заказа {order_id}: {e}") + return None + + +def calculate_discount_for_new_order(partner_id, order_date, current_order_amount_without_markup, + exclude_order_id=None): + """Рассчитывает скидку для нового заказа на основе предыдущих заказов в этом году""" + try: + with get_db() as conn: + year = order_date[:4] if order_date else str(datetime.now().year) + + query = ''' + SELECT id + FROM orders + WHERE partner_id = ? AND strftime('%Y', order_date) = ? + ''' + params = [partner_id, year] + + if exclude_order_id: + query += ' AND id != ?' + params.append(exclude_order_id) + + previous_orders = conn.execute(query, params).fetchall() + + total_previous_without_markup = 0 + for prev_order in previous_orders: + order_total = conn.execute(''' + SELECT COALESCE(SUM(quantity * price_at_order), 0) as total + FROM order_items WHERE order_id = ? + ''', (prev_order['id'],)).fetchone()['total'] + total_previous_without_markup += order_total + + total_with_current_without_markup = total_previous_without_markup + current_order_amount_without_markup + + return calculate_discount(total_with_current_without_markup) + except Exception as e: + print(f"Ошибка при расчете скидки: {e}") + return 0 \ No newline at end of file diff --git a/import_data.py b/import_data.py new file mode 100644 index 0000000..f00c16b --- /dev/null +++ b/import_data.py @@ -0,0 +1,420 @@ +import sqlite3 +import pandas as pd +import os +from database import get_db, create_tables + + +def reset_database(): + """Полностью пересоздает базу данных""" + if os.path.exists('partners.db'): + os.remove('partners.db') + create_tables() + print("✅ База данных пересоздана") + + +def import_data_from_files(): + """Загружает данные из Excel файлов в базу данных""" + try: + # Сначала пересоздаем базу данных + reset_database() + + print("\n📁 Введите имена файлов (или нажмите Enter для использования файлов по умолчанию):") + + partners_file = input("Файл с партнерами (Partners_import.xlsx): ").strip() + if not partners_file: + partners_file = "data/Partners_import.xlsx" + + product_types_file = input("Файл с типами товаров (Product_types_import.xlsx): ").strip() + if not product_types_file: + product_types_file = "data/Product_types_import.xlsx" + + products_file = input("Файл с товарами (Products_import.xlsx): ").strip() + if not products_file: + products_file = "data/Products_import.xlsx" + + orders_file = input("Файл с заказами (Orders_import.xlsx): ").strip() + if not orders_file: + orders_file = "data/Orders_import.xlsx" + + order_items_file = input("Файл с позициями заказов (Order_items_import.xlsx): ").strip() + if not order_items_file: + order_items_file = "data/Order_items_import.xlsx" + + # Проверяем существование файлов + files_to_check = [partners_file, product_types_file, products_file, orders_file, order_items_file] + for file in files_to_check: + if not os.path.exists(file): + print(f"❌ Файл не найден: {file}") + return + + with get_db() as conn: + # Включаем поддержку внешних ключей + conn.execute("PRAGMA foreign_keys = ON") + + # 1. Загрузка типов товаров + print("\n1. 📦 Загрузка типов товаров...") + types_df = pd.read_excel(product_types_file) + + type_name_col = None + for col in ['type_name', 'Тип товара', 'name', 'название']: + if col in types_df.columns: + type_name_col = col + break + + description_col = None + for col in ['description', 'Описание', 'desc']: + if col in types_df.columns: + description_col = col + break + + if not type_name_col: + print("❌ Не найден столбец с названием типа товара") + return + + type_count = 0 + for _, row in types_df.iterrows(): + try: + type_name = str(row[type_name_col]) + description = str(row[description_col]) if description_col and pd.notna( + row.get(description_col)) else '' + + conn.execute(''' + INSERT INTO product_types (type_name, description) + VALUES (?, ?) + ''', (type_name, description)) + type_count += 1 + except Exception as e: + print(f" ⚠️ Ошибка при загрузке типа товара: {e}") + continue + print(f" Загружено {type_count} типов товаров") + + # 2. Загрузка партнеров + print("2. 🏢 Загрузка партнеров...") + partners_df = pd.read_excel(partners_file) + + partner_cols = {} + + for col in ['partner_name', 'Наименование партнера', 'name', 'supplier_name', 'название']: + if col in partners_df.columns: + partner_cols['name'] = col + break + + for col in ['director', 'Директор']: + if col in partners_df.columns: + partner_cols['director'] = col + break + + for col in ['email', 'Электронная почта', 'почта']: + if col in partners_df.columns: + partner_cols['email'] = col + break + + for col in ['phone', 'Телефон']: + if col in partners_df.columns: + partner_cols['phone'] = col + break + + for col in ['legal_address', 'Юридический адрес', 'address', 'адрес']: + if col in partners_df.columns: + partner_cols['address'] = col + break + + for col in ['inn', 'ИНН']: + if col in partners_df.columns: + partner_cols['inn'] = col + break + + for col in ['rating', 'Рейтинг']: + if col in partners_df.columns: + partner_cols['rating'] = col + break + + if 'name' not in partner_cols or 'email' not in partner_cols or 'inn' not in partner_cols: + print("❌ Не найдены обязательные колонки для партнеров") + print(" Доступные колонки:", list(partners_df.columns)) + return + + partner_count = 0 + for _, row in partners_df.iterrows(): + try: + name = str(row[partner_cols['name']]) + email = str(row[partner_cols['email']]) + inn = str(row[partner_cols['inn']]) + + director = str(row.get(partner_cols.get('director'), '')) if 'director' in partner_cols else '' + phone = str(row.get(partner_cols.get('phone'), '')) if 'phone' in partner_cols else '' + address = str(row.get(partner_cols.get('address'), '')) if 'address' in partner_cols else '' + + rating_val = row.get(partner_cols.get('rating'), 0) if 'rating' in partner_cols else 0 + try: + rating = float(rating_val) if rating_val else 0.0 + except: + rating = 0.0 + + conn.execute(''' + INSERT INTO partners + (name, director, email, phone, legal_address, inn, rating) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (name, director, email, phone, address, inn, rating)) + partner_count += 1 + except Exception as e: + print(f" ⚠️ Ошибка при загрузке партнера: {e}") + continue + + print(f" Загружено {partner_count} партнеров") + + # 3. Загрузка товаров + print("3. 📝 Загрузка товаров...") + products_df = pd.read_excel(products_file) + + print(" Доступные колонки в файле товаров:", list(products_df.columns)) + + product_cols = {} + + for col in ['product_type_id', 'ID типа', 'type_id', 'тип_id']: + if col in products_df.columns: + product_cols['type_id'] = col + break + + for col in ['product_name', 'Наименование', 'name', 'товар']: + if col in products_df.columns: + product_cols['name'] = col + break + + for col in ['article', 'Артикул', 'артикул']: + if col in products_df.columns: + product_cols['article'] = col + break + + for col in ['min_price', 'Минимальная цена', 'price', 'цена', 'min_partner_price']: + if col in products_df.columns: + product_cols['price'] = col + break + + for col in ['stock_quantity', 'Остаток', 'stock', 'quantity_in_stock', 'количество']: + if col in products_df.columns: + product_cols['stock'] = col + break + + missing_cols = [] + if 'type_id' not in product_cols: + missing_cols.append('ID типа товара (product_type_id)') + if 'name' not in product_cols: + missing_cols.append('Название товара (product_name)') + if 'article' not in product_cols: + missing_cols.append('Артикул (article)') + if 'price' not in product_cols: + missing_cols.append('Цена (min_price)') + + if missing_cols: + print(f"❌ Не найдены обязательные колонки: {', '.join(missing_cols)}") + return + + # Получаем все существующие ID типов товаров + existing_type_ids = [row[0] for row in conn.execute('SELECT id FROM product_types').fetchall()] + print(f" Существующие ID типов товаров: {existing_type_ids}") + + products_loaded = 0 + for index, row in products_df.iterrows(): + try: + product_type_id = int(row[product_cols['type_id']]) + product_name = str(row[product_cols['name']]) + article = str(row[product_cols['article']]) + + price_val = row[product_cols['price']] + if pd.isna(price_val): + print(f" ⚠️ Пропуск товара {product_name}: пустая цена") + continue + price = float(price_val) + + stock_val = row.get(product_cols.get('stock'), 0) if 'stock' in product_cols else 0 + if pd.isna(stock_val): + stock = 0 + else: + stock = int(stock_val) + + if price <= 0: + print(f" ⚠️ Некорректная цена для товара {product_name}: {price}") + continue + + if product_type_id not in existing_type_ids: + print(f" ⚠️ Тип товара с ID {product_type_id} не найден. Доступные ID: {existing_type_ids}") + continue + + conn.execute(''' + INSERT INTO products + (product_type_id, product_name, article, min_partner_price, quantity_in_stock) + VALUES (?, ?, ?, ?, ?) + ''', (product_type_id, product_name, article, price, max(0, stock))) + products_loaded += 1 + print(f" ✓ Загружен товар: {product_name} (ID типа: {product_type_id})") + + except Exception as e: + print(f" ⚠️ Ошибка при загрузке товара в строке {index + 2}: {e}") + continue + + print(f" Загружено {products_loaded} товаров") + + # 4. Загрузка заказов + print("4. 📋 Загрузка заказов...") + orders_df = pd.read_excel(orders_file) + + print(" Доступные колонки в файле заказов:", list(orders_df.columns)) + + order_cols = {} + + for col in ['partner_id', 'ID партнера', 'partner', 'партнер']: + if col in orders_df.columns: + order_cols['partner_id'] = col + break + + for col in ['order_date', 'Дата заказа', 'date', 'дата']: + if col in orders_df.columns: + order_cols['date'] = col + break + + for col in ['status', 'Статус']: + if col in orders_df.columns: + order_cols['status'] = col + break + + for col in ['notes', 'Примечания', 'note']: + if col in orders_df.columns: + order_cols['notes'] = col + break + + if 'partner_id' not in order_cols or 'date' not in order_cols: + print("❌ Не найдены обязательные колонки для заказов") + return + + existing_partner_ids = [row[0] for row in conn.execute('SELECT id FROM partners').fetchall()] + + orders_loaded = 0 + for _, row in orders_df.iterrows(): + try: + partner_id = int(row[order_cols['partner_id']]) + + if partner_id not in existing_partner_ids: + print(f" ⚠️ Партнер с ID {partner_id} не найден") + continue + + order_date = row[order_cols['date']] + if isinstance(order_date, pd.Timestamp): + order_date = order_date.strftime('%Y-%m-%d') + else: + order_date = str(order_date) + + status = str(row.get(order_cols.get('status'), 'новый')) if 'status' in order_cols and pd.notna( + row.get(order_cols['status'])) else 'новый' + notes = str(row.get(order_cols.get('notes'), '')) if 'notes' in order_cols and pd.notna( + row.get(order_cols['notes'])) else '' + + conn.execute(''' + INSERT INTO orders (partner_id, order_date, status, notes, discount_percent) + VALUES (?, ?, ?, ?, 0) + ''', (partner_id, order_date, status, notes)) + orders_loaded += 1 + print(f" ✓ Загружен заказ: ID {partner_id}, дата {order_date}") + + except Exception as e: + print(f" ⚠️ Ошибка при загрузке заказа: {e}") + continue + + print(f" Загружено {orders_loaded} заказов") + + # 5. Загрузка позиций заказов + print("5. 📦 Загрузка позиций заказов...") + items_df = pd.read_excel(order_items_file) + + print(" Доступные колонки в файле позиций:", list(items_df.columns)) + + item_cols = {} + + for col in ['order_id', 'Номер заказа', 'order', 'заказ']: + if col in items_df.columns: + item_cols['order_id'] = col + break + + for col in ['product_id', 'ID товара', 'product', 'товар']: + if col in items_df.columns: + item_cols['product_id'] = col + break + + for col in ['quantity', 'Количество', 'qty', 'кол']: + if col in items_df.columns: + item_cols['quantity'] = col + break + + for col in ['price', 'Цена', 'price_at_order']: + if col in items_df.columns: + item_cols['price'] = col + break + + missing_cols = [] + if 'order_id' not in item_cols: + missing_cols.append('order_id') + if 'product_id' not in item_cols: + missing_cols.append('product_id') + if 'quantity' not in item_cols: + missing_cols.append('quantity') + if 'price' not in item_cols: + missing_cols.append('price') + + if missing_cols: + print(f"❌ Не найдены обязательные колонки: {', '.join(missing_cols)}") + return + + existing_order_ids = [row[0] for row in conn.execute('SELECT id FROM orders').fetchall()] + existing_product_ids = [row[0] for row in conn.execute('SELECT id FROM products').fetchall()] + + items_loaded = 0 + for index, row in items_df.iterrows(): + try: + order_id = int(row[item_cols['order_id']]) + product_id = int(row[item_cols['product_id']]) + quantity = int(row[item_cols['quantity']]) + price = float(row[item_cols['price']]) + + if quantity <= 0: + print(f" ⚠️ Некорректное количество для заказа {order_id}: {quantity}") + continue + if price <= 0: + print(f" ⚠️ Некорректная цена для заказа {order_id}: {price}") + continue + + if order_id not in existing_order_ids: + print(f" ⚠️ Заказ не найден: {order_id}") + continue + + if product_id not in existing_product_ids: + print(f" ⚠️ Товар с ID {product_id} не найден") + continue + + conn.execute(''' + INSERT INTO order_items (order_id, product_id, quantity, price_at_order) + VALUES (?, ?, ?, ?) + ''', (order_id, product_id, quantity, price)) + items_loaded += 1 + print(f" ✓ Загружена позиция: заказ {order_id}, товар {product_id}") + + except Exception as e: + print(f" ⚠️ Ошибка при загрузке позиции в строке {index + 2}: {e}") + continue + + print(f" Загружено {items_loaded} позиций заказов") + + print(f"\n✅ Данные успешно загружены!") + print(f" Типов товаров: {type_count}") + print(f" Партнеров: {partner_count}") + print(f" Товаров: {products_loaded}") + print(f" Заказов: {orders_loaded}") + print(f" Позиций: {items_loaded}") + + except Exception as e: + print(f"❌ Ошибка при загрузке данных: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + import_data_from_files() \ No newline at end of file diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..7bdfc2f --- /dev/null +++ b/init_db.py @@ -0,0 +1,5 @@ +from database import create_tables + +if __name__ == "__main__": + create_tables() + print("✅ База данных инициализирована") \ No newline at end of file diff --git a/partners.db b/partners.db new file mode 100644 index 0000000..55b7fcf Binary files /dev/null and b/partners.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..900b3fd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.3.3 +pandas==2.0.3 +openpyxl==3.1.2 \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..9a931b3 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,681 @@ +/* Основные стили согласно ТЗ */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Calibri', sans-serif; + background-color: #F5F5F5; /* белый дым */ + color: #333; + line-height: 1.6; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +/* Шапка */ +header { + background-color: #696969; /* темно-серый */ + color: white; + padding: 15px 0; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +header .container { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; +} + +.logo { + display: flex; + align-items: center; + gap: 15px; +} + +.logo img { + height: 50px; + width: auto; +} + +.logo h1 { + font-size: 1.8rem; + font-weight: 500; + color: white; +} + +/* Навигация */ +nav ul { + display: flex; + list-style: none; + gap: 20px; +} + +nav ul li a { + color: white; + text-decoration: none; + padding: 8px 15px; + border-radius: 4px; + transition: background-color 0.3s; + font-weight: 500; +} + +nav ul li a:hover { + background-color: #E0E0E0; /* светло-серый */ + color: #696969; +} + +nav ul li a.active { + background-color: #E0E0E0; + color: #696969; + font-weight: 600; +} + +.btn-add { + background-color: #E0E0E0; + color: #696969 !important; + font-weight: 600; +} + +/* Поиск */ +.search-bar { + flex: 1; + max-width: 400px; + margin-left: 20px; +} + +.search-bar form { + display: flex; + gap: 5px; +} + +.search-bar input, +.search-bar select { + padding: 8px 12px; + border: none; + border-radius: 4px; + font-family: 'Calibri', sans-serif; +} + +.search-bar input { + flex: 1; +} + +.search-bar button { + padding: 8px 15px; + background-color: #E0E0E0; + color: #696969; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + transition: opacity 0.3s; +} + +.search-bar button:hover { + opacity: 0.9; +} + +/* Основной контент */ +main { + flex: 1; + padding: 30px 0; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +.page-header h2 { + font-size: 2rem; + color: #696969; + font-weight: 500; +} + +.header-actions { + display: flex; + gap: 10px; +} + +/* Кнопки */ +.btn-export, +.btn-import, +.btn-view, +.btn-edit, +.btn-back, +.btn-submit { + display: inline-block; + padding: 8px 16px; + text-decoration: none; + border-radius: 4px; + font-weight: 500; + transition: all 0.3s; + border: none; + cursor: pointer; + font-family: 'Calibri', sans-serif; + font-size: 1rem; +} + +.btn-export { + background-color: #696969; + color: white; +} + +.btn-import { + background-color: #E0E0E0; + color: #696969; +} + +.btn-view { + background-color: #F5F5F5; + color: #696969; + border: 1px solid #E0E0E0; +} + +.btn-edit { + background-color: #E0E0E0; + color: #696969; +} + +.btn-back { + background-color: #F5F5F5; + color: #696969; + border: 1px solid #E0E0E0; +} + +.btn-submit { + background-color: #696969; + color: white; + padding: 12px 30px; + font-size: 1.1rem; +} + +.btn-export:hover, +.btn-submit:hover { + background-color: #555; +} + +.btn-import:hover, +.btn-edit:hover { + background-color: #D0D0D0; +} + +/* Карточки заказов */ +.orders-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 25px; +} + +.order-card { + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + transition: transform 0.3s; + display: flex; + flex-direction: column; +} + +.order-card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 20px rgba(0,0,0,0.15); +} + +.order-header { + background-color: #E0E0E0; + padding: 15px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.order-header h3 { + font-size: 1.2rem; + color: #696969; +} + +.order-status { + padding: 3px 8px; + border-radius: 3px; + font-size: 0.85rem; + font-weight: 600; +} + +.status-новый { + background-color: #4CAF50; + color: white; +} + +.status-в_обработке { + background-color: #FFC107; + color: #333; +} + +.status-завершен { + background-color: #2196F3; + color: white; +} + +.order-body { + padding: 15px; + flex: 1; +} + +.order-body p { + margin-bottom: 8px; +} + +.order-items-preview { + margin: 15px 0; + padding: 10px; + background-color: #F5F5F5; + border-radius: 4px; +} + +.order-items-preview ul { + margin-top: 5px; + margin-left: 20px; +} + +.order-total { + margin-top: 15px; + padding-top: 10px; + border-top: 2px solid #E0E0E0; +} + +.subtotal { + font-size: 0.95rem; + color: #666; +} + +.discount { + color: #4CAF50; + font-size: 0.95rem; +} + +.total { + font-size: 1.2rem; + margin-top: 5px; +} + +.order-footer { + padding: 15px; + background-color: #F5F5F5; + display: flex; + gap: 10px; +} + +/* Таблицы */ +.items-table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + margin-top: 20px; +} + +.items-table thead { + background-color: #696969; + color: white; +} + +.items-table th, +.items-table td { + padding: 12px 15px; + text-align: left; +} + +.items-table tbody tr { + border-bottom: 1px solid #E0E0E0; +} + +.items-table tbody tr:hover { + background-color: #F5F5F5; +} + +.items-table tfoot { + background-color: #E0E0E0; + font-weight: 500; +} + +.items-table .total-row { + background-color: #696969; + color: white; +} + +.text-right { + text-align: right; +} + +/* Формы */ +.order-form { + background: white; + border-radius: 8px; + padding: 25px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.form-section { + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid #E0E0E0; +} + +.form-section h3 { + color: #696969; + margin-bottom: 20px; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #555; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 10px; + border: 1px solid #E0E0E0; + border-radius: 4px; + font-family: 'Calibri', sans-serif; + font-size: 1rem; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #696969; +} + +.item-row { + margin-bottom: 15px; + padding: 15px; + background-color: #F5F5F5; + border-radius: 4px; +} + +.item-fields { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr auto; + gap: 10px; + align-items: end; +} + +.item-total { + padding-bottom: 10px; + font-weight: 600; + color: #696969; +} + +.btn-add-item, +.btn-remove-item { + padding: 8px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s; +} + +.btn-add-item { + background-color: #696969; + color: white; + width: 100%; + padding: 12px; + margin-top: 10px; +} + +.btn-remove-item { + background-color: #ff6b6b; + color: white; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; +} + +.btn-add-item:hover { + background-color: #555; +} + +.btn-remove-item:hover { + background-color: #ff5252; +} + +.form-actions { + text-align: center; + margin-top: 30px; +} + +/* Карточки товаров и поставщиков */ +.products-grid, +.suppliers-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.product-card, +.supplier-card { + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + transition: transform 0.3s; +} + +.product-card:hover, +.supplier-card:hover { + transform: translateY(-3px); + box-shadow: 0 5px 15px rgba(0,0,0,0.15); +} + +.product-header, +.supplier-header { + background-color: #E0E0E0; + padding: 15px; +} + +.product-header h3, +.supplier-header h3 { + color: #696969; + font-size: 1.2rem; +} + +.product-body, +.supplier-body { + padding: 15px; +} + +.product-body p, +.supplier-body p { + margin-bottom: 8px; +} + +.in-stock { + color: #4CAF50; + font-weight: 600; +} + +.out-of-stock { + color: #ff6b6b; + font-weight: 600; +} + +.product-footer, +.supplier-footer { + padding: 15px; + background-color: #F5F5F5; + text-align: center; +} + +/* Результаты поиска */ +.search-results { + list-style: none; +} + +.search-result-item { + background: white; + border-radius: 8px; + padding: 15px; + margin-bottom: 15px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + transition: transform 0.3s; +} + +.search-result-item:hover { + transform: translateX(5px); + box-shadow: 0 5px 15px rgba(0,0,0,0.15); +} + +.result-type { + display: inline-block; + padding: 3px 8px; + border-radius: 3px; + font-size: 0.8rem; + font-weight: 600; + margin-bottom: 8px; +} + +.result-type.order { + background-color: #2196F3; + color: white; +} + +.result-type.product { + background-color: #4CAF50; + color: white; +} + +.result-type.supplier { + background-color: #FF9800; + color: white; +} + +/* Уведомления */ +.alert { + padding: 12px 20px; + border-radius: 4px; + margin-bottom: 20px; + animation: slideIn 0.3s; +} + +@keyframes slideIn { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.alert-success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.alert-error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.alert-warning { + background-color: #fff3cd; + color: #856404; + border: 1px solid #ffeeba; +} + +/* Пустое состояние */ +.empty-state { + text-align: center; + padding: 60px 20px; + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.empty-state p { + font-size: 1.2rem; + color: #666; + margin-bottom: 20px; +} + +/* Футер */ +footer { + background-color: #696969; + color: white; + text-align: center; + padding: 20px 0; + margin-top: auto; +} + +/* Адаптивность */ +@media (max-width: 768px) { + header .container { + flex-direction: column; + gap: 15px; + } + + nav ul { + flex-wrap: wrap; + justify-content: center; + } + + .search-bar { + max-width: 100%; + margin-left: 0; + } + + .orders-grid { + grid-template-columns: 1fr; + } + + .item-fields { + grid-template-columns: 1fr; + } + + .page-header { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .items-table { + font-size: 0.9rem; + } + + .items-table th, + .items-table td { + padding: 8px 10px; + } +} \ No newline at end of file diff --git a/static/img/logo.png b/static/img/logo.png new file mode 100644 index 0000000..dfc4547 Binary files /dev/null and b/static/img/logo.png differ diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..57b95e4 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,193 @@ +// Основной JavaScript файл + +document.addEventListener('DOMContentLoaded', function() { + // Автоматическое скрытие уведомлений через 5 секунд + const alerts = document.querySelectorAll('.alert'); + alerts.forEach(alert => { + setTimeout(() => { + alert.style.transition = 'opacity 0.5s'; + alert.style.opacity = '0'; + setTimeout(() => alert.remove(), 500); + }, 5000); + }); + + // Валидация форм на клиенте + const forms = document.querySelectorAll('form[data-validate]'); + forms.forEach(form => { + form.addEventListener('submit', function(e) { + if (!validateForm(form)) { + e.preventDefault(); + } + }); + }); + + // Подтверждение удаления + const deleteButtons = document.querySelectorAll('[data-confirm]'); + deleteButtons.forEach(button => { + button.addEventListener('click', function(e) { + if (!confirm(this.dataset.confirm || 'Вы уверены?')) { + e.preventDefault(); + } + }); + }); + + // Форматирование чисел + formatNumbers(); +}); + +function validateForm(form) { + let isValid = true; + const requiredFields = form.querySelectorAll('[required]'); + + requiredFields.forEach(field => { + if (!field.value.trim()) { + showFieldError(field, 'Это поле обязательно для заполнения'); + isValid = false; + } else { + clearFieldError(field); + } + + // Проверка на отрицательные числа + if (field.type === 'number' && field.min && parseFloat(field.value) < parseFloat(field.min)) { + showFieldError(field, `Значение должно быть не меньше ${field.min}`); + isValid = false; + } + + // Проверка email + if (field.type === 'email' && field.value) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(field.value)) { + showFieldError(field, 'Введите корректный email'); + isValid = false; + } + } + }); + + return isValid; +} + +function showFieldError(field, message) { + clearFieldError(field); + + const errorDiv = document.createElement('div'); + errorDiv.className = 'field-error'; + errorDiv.style.color = '#ff6b6b'; + errorDiv.style.fontSize = '0.85rem'; + errorDiv.style.marginTop = '5px'; + errorDiv.textContent = message; + + field.style.borderColor = '#ff6b6b'; + field.parentNode.appendChild(errorDiv); +} + +function clearFieldError(field) { + field.style.borderColor = ''; + const error = field.parentNode.querySelector('.field-error'); + if (error) { + error.remove(); + } +} + +function formatNumbers() { + // Форматирование цен + const priceElements = document.querySelectorAll('.format-price'); + priceElements.forEach(el => { + const price = parseFloat(el.textContent); + if (!isNaN(price)) { + el.textContent = price.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,') + ' ₽'; + } + }); + + // Форматирование количества + const quantityElements = document.querySelectorAll('.format-quantity'); + quantityElements.forEach(el => { + const quantity = parseInt(el.textContent); + if (!isNaN(quantity)) { + el.textContent = quantity.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + } + }); +} + +// Фильтрация таблиц +function filterTable(inputId, tableId) { + const input = document.getElementById(inputId); + const filter = input.value.toUpperCase(); + const table = document.getElementById(tableId); + const rows = table.getElementsByTagName('tr'); + + for (let i = 1; i < rows.length; i++) { + const cells = rows[i].getElementsByTagName('td'); + let found = false; + + for (let j = 0; j < cells.length; j++) { + const cell = cells[j]; + if (cell) { + const textValue = cell.textContent || cell.innerText; + if (textValue.toUpperCase().indexOf(filter) > -1) { + found = true; + break; + } + } + } + + rows[i].style.display = found ? '' : 'none'; + } +} + +// Экспорт в CSV +function exportToCSV(tableId, filename) { + const table = document.getElementById(tableId); + const rows = table.querySelectorAll('tr'); + const csv = []; + + rows.forEach(row => { + const cols = row.querySelectorAll('td, th'); + const rowData = []; + cols.forEach(col => { + rowData.push('"' + col.innerText.replace(/"/g, '""') + '"'); + }); + csv.push(rowData.join(',')); + }); + + const csvContent = csv.join('\n'); + const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + + link.setAttribute('href', url); + link.setAttribute('download', filename || 'export.csv'); + link.style.visibility = 'hidden'; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +// Печать +function printElement(elementId) { + const element = document.getElementById(elementId); + if (!element) return; + + const printWindow = window.open('', '_blank'); + printWindow.document.write(` + + + Печать + + + + + ${element.outerHTML} + + + + `); + printWindow.document.close(); +} \ No newline at end of file diff --git a/templates/add_order.html b/templates/add_order.html new file mode 100644 index 0000000..05b1959 --- /dev/null +++ b/templates/add_order.html @@ -0,0 +1,455 @@ +{% extends "base.html" %} + +{% block content %} + + +
+
+

Информация о заказе

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+

Позиции заказа

+

Выберите товары для заказа

+ +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 0.00 ₽ +
+ + +
+
+
+ + +
+ +
+ +
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/add_partner.html b/templates/add_partner.html new file mode 100644 index 0000000..1535056 --- /dev/null +++ b/templates/add_partner.html @@ -0,0 +1,221 @@ +{% extends "base.html" %} + +{% block content %} + + +
+
+

Основная информация

+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+
+ +
+

Юридическая информация

+ +
+ + +
+ +
+
+ + + 10 или 12 цифр +
+
+
+ +
+ + +
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..f53b3bc --- /dev/null +++ b/templates/base.html @@ -0,0 +1,52 @@ + + + + + + Канцелярские товары - Управление заказами + + + + +
+
+ + +
+
+ +
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+
+ +
+
+

© 2026 Канцелярские товары. Все права защищены.

+

Курсовой проект по МДК 01.01

+
+
+ + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/templates/edit_order.html b/templates/edit_order.html new file mode 100644 index 0000000..4e2ef6d --- /dev/null +++ b/templates/edit_order.html @@ -0,0 +1,412 @@ +{% extends "base.html" %} + +{% block content %} + + +
+
+

Информация о заказе

+ +
+ + +
+ +
+ + +
+
+ +
+

Позиции заказа

+

Выберите товары для заказа

+ +
+ {% for item in items %} +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + {{ "%.2f"|format(item.quantity * item.price_at_order * 1.1) }} ₽ +
+ + +
+
+ {% endfor %} +
+ + +
+ +
+ + Отмена +
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/edit_partner.html b/templates/edit_partner.html new file mode 100644 index 0000000..112a722 --- /dev/null +++ b/templates/edit_partner.html @@ -0,0 +1,239 @@ +{% extends "base.html" %} + +{% block content %} + + +
+
+

Основная информация

+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+
+ +
+

Юридическая информация

+ +
+ + +
+ +
+
+ + + 10 или 12 цифр +
+
+
+ +
+ + + Отмена +
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/import.html b/templates/import.html new file mode 100644 index 0000000..d15681a --- /dev/null +++ b/templates/import.html @@ -0,0 +1,267 @@ +{% extends "base.html" %} + +{% block content %} + + +
+
+

Инструкция по импорту

+
    +
  1. Подготовьте файлы Excel в соответствии с шаблонами
  2. +
  3. Убедитесь, что файлы находятся в папке data/
  4. +
  5. Введите имена файлов в форму ниже
  6. +
  7. Нажмите "Импортировать данные"
  8. +
+ +

Требования к файлам:

+ + + + + + + + + + + + + + + + + + + + + +
ФайлОбязательные колонки
ПоставщикиНаименование поставщика, Электронная почта, ИНН
Типы товаровТип товара
ТоварыТип товара, Наименование, Артикул, Минимальная цена
ЗаказыПоставщик, Дата заказа
+
+ +
+
+

Файлы для импорта

+ +
+ + + Формат: .xlsx, .xls +
+ +
+ + + Формат: .xlsx, .xls +
+ +
+ + + Формат: .xlsx, .xls +
+ +
+ + + Формат: .xlsx, .xls +
+
+ +
+

⚠️ Внимание!

+

Импорт данных очистит существующую базу данных и загрузит новые данные из указанных файлов.

+

Убедитесь, что у вас есть резервная копия важных данных.

+
+ +
+ + +
+
+ +
+

Шаблоны файлов

+

Скачайте примеры файлов для импорта:

+ +
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..5df5a71 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,260 @@ +{% extends "base.html" %} + +{% block content %} + + +{% if orders %} +
+ {% for order in orders %} +
+
+

Заказ #{{ order.order_id }}

+
+
+

Партнер: {{ order.partner_name }}

+

Дата: {{ order.order_date }}

+

Позиций: {{ order.items_count if order.items_count else 0 }}

+ +
+ Информация: +

В заказе {{ order.items_count if order.items_count else 0 }} позиций

+
+ +
+
Сумма без наценки: {{ "%.2f"|format(order.subtotal_without_markup|float) }} ₽
+
Наценка 10%: +{{ "%.2f"|format((order.subtotal_with_markup - order.subtotal_without_markup)|float) }} ₽
+
Сумма с наценкой: {{ "%.2f"|format(order.subtotal_with_markup|float) }} ₽
+ {% if order.discount_percent and order.discount_percent > 0 %} +
Скидка {{ order.discount_percent }}%: -{{ "%.2f"|format(order.discount_amount|float) }} ₽
+ {% endif %} +
ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total|float) }} ₽
+
+
+ +
+ {% endfor %} +
+{% else %} +
+

Заказов пока нет

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

Инструкция

+

Заполните форму ниже и нажмите "Проверить данные". Тест покажет, пройдет ли проверку введенная информация.

+

Правила валидации:

+
    +
  • Наименование партнера - обязательно
  • +
  • Email - должен быть корректным (содержать @ и .)
  • +
  • ИНН - должен содержать 10 или 12 цифр
  • +
  • Телефон - форматируется автоматически
  • +
+
+ +
+
+
+

Данные партнера

+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+
+ + + 10 или 12 цифр +
+
+
+ +
+ + + + +
+
+
+ + +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/order_detail.html b/templates/order_detail.html new file mode 100644 index 0000000..d6a1bb8 --- /dev/null +++ b/templates/order_detail.html @@ -0,0 +1,171 @@ +{% extends "base.html" %} + +{% block content %} + + +
+
+
+ Партнер: + {{ order.partner_name }} +
+
+ Дата заказа: + {{ order.order_date }} +
+
+ Статус: + {{ order.status }} +
+ {% if order.notes %} +
+ Примечания: + {{ order.notes }} +
+ {% endif %} +
+ +

Состав заказа

+ + {% if order.items and order.items|length > 0 %} + + + + + + + + + + + + {% for item in order.items %} + + + + + + + + {% endfor %} + + + + + + + {% if order.discount_percent and order.discount_percent > 0 %} + + + + + {% endif %} + + + + + +
№ТоварКоличествоЦена за ед.Сумма
{{ loop.index }}{{ item.product_name }}{{ item.quantity }}{{ "%.2f"|format(item.price) }} ₽{{ "%.2f"|format(item.subtotal) }} ₽
Сумма без скидки:{{ "%.2f"|format(order.subtotal) }} ₽
Скидка ({{ order.discount_desc }}):-{{ "%.2f"|format(order.discount_amount) }} ₽
ИТОГО:{{ "%.2f"|format(order.total) }} ₽
+ {% else %} +
+

В заказе нет товаров

+ Добавить товары +
+ {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/orders.html b/templates/orders.html new file mode 100644 index 0000000..f0c4216 --- /dev/null +++ b/templates/orders.html @@ -0,0 +1,228 @@ +{% extends "base.html" %} + +{% block content %} + + +
+ + + + + + + +
+ +
+ + + + + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + + + + {% endfor %} + +
№ заказаДатаПоставщикСтатусПозицийСуммаСкидкаИтогоДействия
#{{ order.order_id }}{{ order.order_date }}{{ order.supplier_name }}{{ order.status }}{{ order.items|length }}{{ order.subtotal }} ₽ + {% if order.discount_percent > 0 %} + {{ order.discount_percent }}% (-{{ order.discount_amount }} ₽) + {% else %} + - + {% endif %} + {{ order.total }} ₽ + 👁️ + ✏️ +
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/partners.html b/templates/partners.html new file mode 100644 index 0000000..d1e1987 --- /dev/null +++ b/templates/partners.html @@ -0,0 +1,222 @@ +{% extends "base.html" %} + +{% block content %} + + +
+ +
+ +{% if partners %} +
+ {% for partner in partners %} +
+
+

{{ partner.name }}

+
+
+

Директор: {{ partner.director or 'Не указан' }}

+

ИНН: {{ partner.inn }}

+

Email: {{ partner.email }}

+

Телефон: {{ partner.phone or 'Не указан' }}

+

Адрес: {{ partner.legal_address or 'Не указан' }}

+ +
+

Заказов: {{ partner.orders_count }}

+

Общая сумма закупок: {{ "%.2f"|format(partner.total_purchases) }} ₽

+

Текущая скидка: {{ partner.discount_desc }}

+
+
+ +
+ {% endfor %} +
+{% else %} +
+

Партнеров пока нет

+ Добавить первого партнера +
+{% endif %} + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/products.html b/templates/products.html new file mode 100644 index 0000000..4041db4 --- /dev/null +++ b/templates/products.html @@ -0,0 +1,778 @@ +{% extends "base.html" %} + +{% block content %} + + +{% if products %} +
+ {% for product in products %} +
+
+

{{ product.product_name }}

+ {{ product.product_type_name }} +
+
+

Артикул: {{ product.article }}

+

Цена для партнеров: {{ product.min_partner_price }} ₽

+

Цена с наценкой 10%: {{ "%.2f"|format(product.min_partner_price * 1.1) }} ₽

+
+ +
+ {% endfor %} +
+{% else %} +
+

Товаров пока нет

+ +
+{% endif %} + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/search_results.html b/templates/search_results.html new file mode 100644 index 0000000..11ff240 --- /dev/null +++ b/templates/search_results.html @@ -0,0 +1,131 @@ +{% extends "base.html" %} + +{% block content %} + + +{% if results %} +
+

Найдено: {{ results|length }} результатов

+ +
+ +
    + {% for result in results %} +
  • + {{ result.result_type }} + + {% if result.result_type == 'order' %} +

    {{ result.display_name }}

    +

    Дата: {{ result.order_date }} | Статус: {{ result.status }}

    + {% if result.notes %} +

    {{ result.notes[:100] }}{% if result.notes|length > 100 %}...{% endif %}

    + {% endif %} + + {% elif result.result_type == 'product' %} +

    {{ result.display_name }}

    +

    Артикул: {{ result.article }} | Тип: {{ result.type_name }}

    +

    Цена: {{ result.min_partner_price }} ₽ | Остаток: {{ result.quantity_in_stock }} шт.

    + + {% elif result.result_type == 'supplier' %} +

    {{ result.display_name }}

    +

    ИНН: {{ result.inn }} | Email: {{ result.email }}

    +

    Рейтинг: {{ result.rating }} | Телефон: {{ result.phone or 'Не указан' }}

    + {% endif %} + +
    + {% if result.result_type == 'order' %} + Подробнее + {% elif result.result_type == 'product' %} + + {% elif result.result_type == 'supplier' %} + Редактировать + {% endif %} +
    +
  • + {% endfor %} +
+{% else %} +
+

По запросу "{{ query }}" ничего не найдено

+

Попробуйте изменить поисковый запрос

+
+{% endif %} + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/view_order.html b/templates/view_order.html new file mode 100644 index 0000000..e3be87f --- /dev/null +++ b/templates/view_order.html @@ -0,0 +1,211 @@ +{% extends "base.html" %} + +{% block content %} + + +
+
+
+ Партнер: + {{ order.partner_name }} +
+
+ Дата заказа: + {{ order.order_date }} +
+ {% if order.notes %} +
+ Примечания: + {{ order.notes }} +
+ {% endif %} +
+ +

Состав заказа

+ + {% if order.items and order.items|length > 0 %} + + + + + + + + + + + + + {% for item in order.items %} + + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + {% if order.discount_percent and order.discount_percent > 0 %} + + + + + + {% endif %} + + + + + + +
№ТоварКоличествоЦена за ед.Базовая суммаСумма с наценкой 10%
{{ loop.index }}{{ item.product_name }}{{ item.quantity }}{{ "%.2f"|format(item.price) }} ₽{{ "%.2f"|format(item.base_subtotal) }} ₽{{ "%.2f"|format(item.subtotal_with_markup) }} ₽
Итого без наценки:{{ "%.2f"|format(order.subtotal_without_markup) }} ₽
Наценка 10%:+{{ "%.2f"|format(order.subtotal_with_markup - order.subtotal_without_markup) }} ₽
Сумма с наценкой:{{ "%.2f"|format(order.subtotal_with_markup) }} ₽
Скидка ({{ order.discount_desc }}):-{{ "%.2f"|format(order.discount_amount) }} ₽
ИТОГО К ОПЛАТЕ:{{ "%.2f"|format(order.total) }} ₽
+ {% else %} +
+

В заказе нет товаров

+
+ {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/Order_items_import.xlsx b/tests/data/Order_items_import.xlsx new file mode 100644 index 0000000..5cc8a50 Binary files /dev/null and b/tests/data/Order_items_import.xlsx differ diff --git a/tests/data/Orders_import.xlsx b/tests/data/Orders_import.xlsx new file mode 100644 index 0000000..be3ec02 Binary files /dev/null and b/tests/data/Orders_import.xlsx differ diff --git a/tests/data/Partners_import.xlsx b/tests/data/Partners_import.xlsx new file mode 100644 index 0000000..5ee0a6c Binary files /dev/null and b/tests/data/Partners_import.xlsx differ diff --git a/tests/data/Product_types_import.xlsx b/tests/data/Product_types_import.xlsx new file mode 100644 index 0000000..05d914e Binary files /dev/null and b/tests/data/Product_types_import.xlsx differ diff --git a/tests/data/Products_import.xlsx b/tests/data/Products_import.xlsx new file mode 100644 index 0000000..d6792e6 Binary files /dev/null and b/tests/data/Products_import.xlsx differ diff --git a/tests/test_calculation.py b/tests/test_calculation.py new file mode 100644 index 0000000..d4c77a4 --- /dev/null +++ b/tests/test_calculation.py @@ -0,0 +1,60 @@ +import unittest +import sys +import os + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database import calculate_discount, get_discount_description + + +class TestCalculations(unittest.TestCase): + """Тестирование алгоритмов расчета скидок""" + + def test_1_discount_thresholds(self): + """Тест пороговых значений скидок""" + test_cases = [ + (0, 0), + (100000, 0), + (149999, 0), + (150000, 15), + (175000, 15), + (199999, 15), + (200000, 30), + (250000, 30), + (299999, 30), + (300000, 50), + (500000, 50), + (1000000, 50), + ] + + for amount, expected_discount in test_cases: + with self.subTest(amount=amount): + self.assertEqual(calculate_discount(amount), expected_discount) + + def test_2_discount_descriptions(self): + """Тест описаний скидок""" + self.assertEqual(get_discount_description(0), '0% (до 150 000 руб.)') + self.assertEqual(get_discount_description(15), '15% (150 000 - 199 999 руб.)') + self.assertEqual(get_discount_description(30), '30% (200 000 - 299 999 руб.)') + self.assertEqual(get_discount_description(50), '50% (300 000+ руб.)') + + self.assertEqual(get_discount_description(99), '0% (до 150 000 руб.)') + + def test_3_edge_cases(self): + """Тест граничных случаев""" + self.assertEqual(calculate_discount(150000), 15) + self.assertEqual(calculate_discount(200000), 30) + self.assertEqual(calculate_discount(300000), 50) + + self.assertEqual(calculate_discount(149999), 0) + self.assertEqual(calculate_discount(199999), 15) + self.assertEqual(calculate_discount(299999), 30) + + def test_4_negative_amounts(self): + """Тест отрицательных сумм""" + self.assertEqual(calculate_discount(-1000), 0) + self.assertEqual(calculate_discount(-150000), 0) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_database.py b/tests/test_database.py new file mode 100644 index 0000000..03762be --- /dev/null +++ b/tests/test_database.py @@ -0,0 +1,130 @@ +import unittest +import sqlite3 +import os +import sys +import tempfile + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database import get_db_connection, create_tables, calculate_discount, get_discount_description, \ + calculate_discount_for_new_order + + +class TestDatabase(unittest.TestCase): + """Тестирование функций базы данных""" + + def setUp(self): + """Создаем временную базу данных перед каждым тестом""" + self.db_fd, self.db_path = tempfile.mkstemp() + + # Подменяем путь к БД + import database + database.DATABASE = self.db_path + + # Создаем таблицы + create_tables() + + # Добавляем тестовые данные + self.conn = sqlite3.connect(self.db_path) + self.conn.row_factory = sqlite3.Row + cursor = self.conn.cursor() + + # Добавляем типы товаров + cursor.executemany(''' + INSERT INTO product_types (type_name, description) VALUES (?, ?) + ''', [ + ('Ручки', 'Пишущие принадлежности'), + ('Тетради', 'Школьные тетради') + ]) + + # Добавляем партнеров (БЕЗ rating) + cursor.executemany(''' + INSERT INTO partners (name, director, email, phone, legal_address, inn) + VALUES (?, ?, ?, ?, ?, ?) + ''', [ + ('ООО Тест', 'Иванов И.И.', 'test@test.ru', '+7(999)123-45-67', + 'г. Москва', '7701123456'), + ('ИП Тестов', 'Петров П.П.', 'test2@test.ru', '+7(999)765-43-21', + 'г. Москва', '7701765432') + ]) + + # Добавляем товары (БЕЗ quantity_in_stock) + cursor.executemany(''' + INSERT INTO products (product_type_id, product_name, article, min_partner_price) + VALUES (?, ?, ?, ?) + ''', [ + (1, 'Ручка тестовая', 'R001', 100.0), + (2, 'Тетрадь тестовая', 'T001', 200.0) + ]) + + # Добавляем заказы (БЕЗ status) + cursor.execute(''' + INSERT INTO orders (partner_id, order_date, notes, discount_percent) + VALUES (?, ?, ?, ?) + ''', (1, '2026-01-15', 'Тестовый заказ', 0)) + + cursor.execute(''' + INSERT INTO orders (partner_id, order_date, notes, discount_percent) + VALUES (?, ?, ?, ?) + ''', (1, '2026-02-15', 'Второй заказ', 15)) + + cursor.execute(''' + INSERT INTO orders (partner_id, order_date, notes, discount_percent) + VALUES (?, ?, ?, ?) + ''', (2, '2026-03-15', 'Третий заказ', 30)) + + # Добавляем позиции заказов + cursor.executemany(''' + INSERT INTO order_items (order_id, product_id, quantity, price_at_order) + VALUES (?, ?, ?, ?) + ''', [ + (1, 1, 10, 100.0), + (1, 2, 5, 200.0), + (2, 1, 20, 100.0), + (3, 2, 15, 200.0) + ]) + + self.conn.commit() + + def tearDown(self): + """Закрываем и удаляем временную базу данных""" + self.conn.close() + os.close(self.db_fd) + os.unlink(self.db_path) + + def test_1_connection(self): + """Тест подключения к базе данных""" + conn = get_db_connection() + self.assertIsNotNone(conn) + conn.close() + + def test_2_calculate_discount(self): + """Тест расчета скидки""" + self.assertEqual(calculate_discount(100000), 0) + self.assertEqual(calculate_discount(150000), 15) + self.assertEqual(calculate_discount(200000), 30) + self.assertEqual(calculate_discount(300000), 50) + self.assertEqual(calculate_discount(500000), 50) + + def test_3_get_discount_description(self): + """Тест описания скидки""" + self.assertEqual(get_discount_description(0), '0% (до 150 000 руб.)') + self.assertEqual(get_discount_description(15), '15% (150 000 - 199 999 руб.)') + self.assertEqual(get_discount_description(30), '30% (200 000 - 299 999 руб.)') + self.assertEqual(get_discount_description(50), '50% (300 000+ руб.)') + + def test_4_calculate_discount_for_new_order(self): + """Тест расчета скидки для нового заказа""" + # Первый заказ (сумма 10000) - без скидки + discount = calculate_discount_for_new_order(1, '2026-04-01', 10000) + self.assertEqual(discount, 0) + + # Рассчитаем общую сумму предыдущих заказов для партнера 1 + # Заказы: 1 (10*100 + 5*200 = 2000), 2 (20*100 = 2000), итого 4000 + # Новый заказ 146000 - должно быть 15% скидка (4000 + 146000 = 150000) + discount = calculate_discount_for_new_order(1, '2026-04-03', 146000) + self.assertEqual(discount, 15) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..4b1cf51 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,272 @@ +import unittest +import sqlite3 +import os +import sys +import tempfile +import json +import time +from datetime import datetime + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database import get_db_connection, create_tables, calculate_discount_for_new_order, calculate_order_total, get_db +from app import app + + +class TestIntegration(unittest.TestCase): + """Интеграционное тестирование всего приложения""" + + @classmethod + def setUpClass(cls): + """Создаем тестовую базу данных""" + cls.db_fd, cls.db_path = tempfile.mkstemp() + + import database + database.DATABASE = cls.db_path + + create_tables() + + cls.conn = sqlite3.connect(cls.db_path) + cls.conn.row_factory = sqlite3.Row + cursor = cls.conn.cursor() + + # Добавляем типы товаров + cursor.executemany(''' + INSERT INTO product_types (type_name, description) VALUES (?, ?) + ''', [ + ('Ручки', 'Пишущие принадлежности'), + ('Тетради', 'Школьные тетради'), + ('Бумага', 'Офисная бумага'), + ('Техника', 'Дорогая техника') + ]) + + # Добавляем партнеров (БЕЗ rating) + cursor.executemany(''' + INSERT INTO partners (name, director, email, phone, legal_address, inn) + VALUES (?, ?, ?, ?, ?, ?) + ''', [ + ('ООО КанцПром', 'Иванов И.И.', 'info@kancprom.ru', '+7(495)123-45-67', + 'г. Москва, ул. Промышленная', '7701123456'), + ('ИП Петров', 'Петров А.С.', 'petrov@mail.ru', '+7(495)765-43-21', + 'г. Москва, ул. Торговая', '7701765432') + ]) + + # Добавляем товары (БЕЗ quantity_in_stock) + cursor.executemany(''' + INSERT INTO products (product_type_id, product_name, article, min_partner_price) + VALUES (?, ?, ?, ?) + ''', [ + (1, 'Ручка синяя', 'R001', 15.50), + (1, 'Ручка красная', 'R002', 12.00), + (2, 'Тетрадь 48л', 'T001', 45.00), + (2, 'Тетрадь 96л', 'T002', 85.00), + (3, 'Бумага А4', 'B001', 280.00), + (4, 'Принтер лазерный', 'P001', 15000.00) + ]) + + cls.conn.commit() + + def test_db_connection(): + conn = sqlite3.connect(cls.db_path) + conn.row_factory = sqlite3.Row + return conn + + app.config['DATABASE'] = cls.db_path + database.get_db_connection = test_db_connection + + @classmethod + def tearDownClass(cls): + cls.conn.close() + os.close(cls.db_fd) + time.sleep(0.5) + try: + os.unlink(cls.db_path) + except: + pass + + def setUp(self): + self.app = app.test_client() + self.app.testing = True + + with sqlite3.connect(self.db_path) as conn: + conn.execute("DELETE FROM order_items") + conn.execute("DELETE FROM orders") + + def test_1_full_order_flow(self): + """Тест полного цикла создания заказа""" + order_data = { + 'partner_id': 1, + 'order_date': '2026-03-04', + 'notes': 'Тестовый заказ', + 'items': [ + {'product_id': 1, 'quantity': 10, 'price': 15.50}, + {'product_id': 3, 'quantity': 5, 'price': 45.00} + ] + } + + response = self.app.post('/api/order', + data=json.dumps(order_data), + content_type='application/json') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + self.assertTrue(data['success']) + order_id = data['order_id'] + + with sqlite3.connect(self.db_path) as conn: + conn.row_factory = sqlite3.Row + order = conn.execute('SELECT * FROM orders WHERE id = ?', (order_id,)).fetchone() + self.assertIsNotNone(order) + self.assertEqual(order['partner_id'], 1) + + items = conn.execute('SELECT * FROM order_items WHERE order_id = ?', (order_id,)).fetchall() + self.assertEqual(len(items), 2) + + def test_2_discount_accumulation(self): + """Тест накопления скидки""" + order1_data = { + 'partner_id': 2, + 'order_date': '2026-03-04', + 'notes': 'Первый заказ', + 'items': [ + {'product_id': 6, 'quantity': 7, 'price': 15000.00} # 7 * 15000 = 105 000 + ] + } + + response = self.app.post('/api/order', + data=json.dumps(order1_data), + content_type='application/json') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + order1_id = data['order_id'] + + order1_total = calculate_order_total(order1_id) + self.assertEqual(order1_total['discount_percent'], 0) + + order2_data = { + 'partner_id': 2, + 'order_date': '2026-03-05', + 'notes': 'Второй заказ', + 'items': [ + {'product_id': 6, 'quantity': 4, 'price': 15000.00} # 4 * 15000 = 60 000 (всего 165 000) + ] + } + + response = self.app.post('/api/order', + data=json.dumps(order2_data), + content_type='application/json') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + order2_id = data['order_id'] + + order2_total = calculate_order_total(order2_id) + self.assertEqual(order2_total['discount_percent'], 15) + + def test_3_discount_reset_next_year(self): + """Тест сброса скидки в новом году""" + order_2026_data = { + 'partner_id': 1, + 'order_date': '2026-12-01', + 'notes': 'Заказ 2026', + 'items': [ + {'product_id': 6, 'quantity': 14, 'price': 15000.00} # 14 * 15000 = 210 000 + ] + } + + response = self.app.post('/api/order', + data=json.dumps(order_2026_data), + content_type='application/json') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + order_2026_id = data['order_id'] + + order_2026_total = calculate_order_total(order_2026_id) + self.assertEqual(order_2026_total['discount_percent'], 30) + + order_2027_data = { + 'partner_id': 1, + 'order_date': '2027-01-15', + 'notes': 'Заказ 2027', + 'items': [ + {'product_id': 6, 'quantity': 7, 'price': 15000.00} # 7 * 15000 = 105 000 + ] + } + + response = self.app.post('/api/order', + data=json.dumps(order_2027_data), + content_type='application/json') + + self.assertEqual(response.status_code, 200) + data = json.loads(response.data) + order_2027_id = data['order_id'] + + order_2027_total = calculate_order_total(order_2027_id) + self.assertEqual(order_2027_total['discount_percent'], 0) + + def test_4_negative_quantity_prevention(self): + """Тест предотвращения отрицательного количества""" + order_data = { + 'partner_id': 1, + 'order_date': '2026-03-04', + 'items': [ + {'product_id': 1, 'quantity': -5, 'price': 15.50} + ] + } + + response = self.app.post('/api/order', + data=json.dumps(order_data), + content_type='application/json') + + self.assertEqual(response.status_code, 400) + data = json.loads(response.data) + self.assertFalse(data['success']) + + def test_5_no_product_deletion_endpoint(self): + """Тест отсутствия возможности удаления товара""" + # Сначала создаем заказ, чтобы товар был в заказах + order_data = { + 'partner_id': 1, + 'order_date': '2026-03-04', + 'notes': 'Заказ для теста', + 'items': [ + {'product_id': 1, 'quantity': 5, 'price': 15.50} + ] + } + + response = self.app.post('/api/order', + data=json.dumps(order_data), + content_type='application/json') + + self.assertEqual(response.status_code, 200) + + # Пытаемся получить доступ к несуществующему маршруту удаления + # Вместо DELETE используем GET, чтобы проверить, что маршрут не существует + response = self.app.get('/api/product/1/delete') + + # Должен быть 404 (не найдено) + self.assertEqual(response.status_code, 404) + + # Проверяем, что товар все еще существует в базе + from database import get_db + with get_db() as conn: + product = conn.execute('SELECT * FROM products WHERE id = 1').fetchone() + self.assertIsNotNone(product) + + # Проверяем, что API не поддерживает метод DELETE для товаров + # Отправляем DELETE запрос на существующий маршрут /api/product/1 + response = self.app.delete('/api/product/1') + + # Должен быть либо 405 (Method Not Allowed), либо 404 (Not Found) + self.assertIn(response.status_code, [404, 405]) + + # Проверяем, что товар все еще существует + with get_db() as conn: + product = conn.execute('SELECT * FROM products WHERE id = 1').fetchone() + self.assertIsNotNone(product) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_orders.py b/tests/test_orders.py new file mode 100644 index 0000000..78ee335 --- /dev/null +++ b/tests/test_orders.py @@ -0,0 +1,230 @@ +import unittest +import sqlite3 +import os +import sys +import tempfile +import time +from datetime import datetime + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database import get_db_connection, calculate_order_total +from app import app + + +class TestOrders(unittest.TestCase): + """Тестирование функционала заказов""" + + @classmethod + def setUpClass(cls): + """Создаем тестовую базу данных""" + cls.db_fd, cls.db_path = tempfile.mkstemp() + + import database + database.DATABASE = cls.db_path + + from database import create_tables + create_tables() + + cls.conn = sqlite3.connect(cls.db_path) + cls.conn.row_factory = sqlite3.Row + cursor = cls.conn.cursor() + + # Добавляем типы товаров + cursor.execute("INSERT INTO product_types (type_name) VALUES ('Ручки')") + cursor.execute("INSERT INTO product_types (type_name) VALUES ('Тетради')") + + # Добавляем партнеров (БЕЗ rating) + cursor.execute(''' + INSERT INTO partners (name, email, inn) VALUES + ('ООО Тест', 'test@test.ru', '1234567890') + ''') + cursor.execute(''' + INSERT INTO partners (name, email, inn) VALUES + ('ИП Тест', 'test2@test.ru', '0987654321') + ''') + + # Добавляем товары (БЕЗ quantity_in_stock) + cursor.execute(''' + INSERT INTO products (product_type_id, product_name, article, min_partner_price) + VALUES (1, 'Ручка синяя', 'R001', 50.0) + ''') + cursor.execute(''' + INSERT INTO products (product_type_id, product_name, article, min_partner_price) + VALUES (1, 'Ручка красная', 'R002', 60.0) + ''') + cursor.execute(''' + INSERT INTO products (product_type_id, product_name, article, min_partner_price) + VALUES (2, 'Тетрадь 48л', 'T001', 100.0) + ''') + + # Добавляем заказы (БЕЗ status) + cursor.execute(''' + INSERT INTO orders (partner_id, order_date, discount_percent) + VALUES (1, '2026-01-01', 0) + ''') + cursor.execute(''' + INSERT INTO orders (partner_id, order_date, discount_percent) + VALUES (1, '2026-02-01', 15) + ''') + + # Добавляем позиции заказов + cursor.execute(''' + INSERT INTO order_items (order_id, product_id, quantity, price_at_order) + VALUES (1, 1, 10, 50.0) + ''') + cursor.execute(''' + INSERT INTO order_items (order_id, product_id, quantity, price_at_order) + VALUES (1, 3, 5, 100.0) + ''') + cursor.execute(''' + INSERT INTO order_items (order_id, product_id, quantity, price_at_order) + VALUES (2, 2, 20, 60.0) + ''') + + cls.conn.commit() + + def test_db_connection(): + conn = sqlite3.connect(cls.db_path) + conn.row_factory = sqlite3.Row + return conn + + app.config['DATABASE'] = cls.db_path + database.get_db_connection = test_db_connection + + @classmethod + def tearDownClass(cls): + cls.conn.close() + os.close(cls.db_fd) + try: + os.unlink(cls.db_path) + except: + pass + + def setUp(self): + self.app = app.test_client() + self.app.testing = True + + def test_1_calculate_order_total(self): + """Тест расчета общей суммы заказа с наценкой 10%""" + result = calculate_order_total(1) + self.assertIsNotNone(result) + # 10*50 + 5*100 = 1000 базовая сумма + self.assertEqual(result['subtotal_without_markup'], 1000.0) + # С наценкой 10%: 1000 * 1.1 = 1100 + self.assertEqual(result['subtotal_with_markup'], 1100.0) + self.assertEqual(result['discount_percent'], 0) + self.assertEqual(result['total'], 1100.0) + + result = calculate_order_total(2) + self.assertIsNotNone(result) + # 20*60 = 1200 базовая сумма + self.assertEqual(result['subtotal_without_markup'], 1200.0) + # С наценкой: 1200 * 1.1 = 1320 + self.assertEqual(result['subtotal_with_markup'], 1320.0) + # Со скидкой 15%: 1320 * 0.85 = 1122 + self.assertEqual(result['discount_percent'], 15) + self.assertEqual(result['discount_amount'], 198.0) + self.assertEqual(result['total'], 1122.0) + + def test_2_create_order_page(self): + """Тест страницы создания заказа""" + response = self.app.get('/add_order') + self.assertEqual(response.status_code, 200) + self.assertIn('Создание нового заказа', response.data.decode('utf-8')) + + def test_3_edit_order_page(self): + """Тест страницы редактирования заказа""" + response = self.app.get('/edit_order/1') + self.assertEqual(response.status_code, 200) + self.assertIn('Редактирование заказа #1', response.data.decode('utf-8')) + + def test_4_edit_order_without_items(self): + """Тест редактирования заказа без изменения позиций (только дата/примечания)""" + response = self.app.post('/edit_order/1', data={ + 'order_date': '2026-03-05', + 'notes': 'Обновлён без товаров' + }, follow_redirects=True) + + self.assertEqual(response.status_code, 200) + self.assertIn('Заказ обновлен', response.data.decode('utf-8')) + + from database import get_db + with get_db() as conn: + count = conn.execute('SELECT COUNT(*) FROM order_items WHERE order_id = 1').fetchone()[0] + self.assertEqual(count, 2) + + def test_5_edit_order_add_new_item(self): + """Тест добавления новой позиции при редактировании""" + response = self.app.post('/edit_order/1', data={ + 'order_date': '2026-03-05', + 'notes': 'Добавлен новый товар', + 'product_id[]': ['1', '2', '2'], + 'quantity[]': ['5', '2', '3'], + 'price[]': ['50.0', '60.0', '60.0'] + }, follow_redirects=True) + + self.assertEqual(response.status_code, 200) + self.assertIn('Заказ обновлен', response.data.decode('utf-8')) + + from database import get_db + with get_db() as conn: + new_count = conn.execute('SELECT COUNT(*) FROM order_items WHERE order_id = 1').fetchone()[0] + items = conn.execute('SELECT product_id, quantity FROM order_items WHERE order_id = 1').fetchall() + + self.assertEqual(new_count, 3) + + expected_items = [ + (1, 5), + (2, 2), + (2, 3) + ] + + for i, (expected_product_id, expected_quantity) in enumerate(expected_items): + self.assertEqual(items[i]['product_id'], expected_product_id) + self.assertEqual(items[i]['quantity'], expected_quantity) + + def test_6_no_delete_button(self): + """Тест отсутствия кнопки удаления на странице редактирования заказа""" + # Сначала создаем заказ + response = self.app.post('/add_order', data={ + 'partner_id': '1', + 'order_date': '2026-03-04', + 'notes': 'Заказ для теста', + 'product_id[]': ['1'], + 'quantity[]': ['1'], + 'price[]': ['50.0'] + }, follow_redirects=True) + + self.assertEqual(response.status_code, 200) + self.assertIn('Заказ успешно создан', response.data.decode('utf-8')) + + # Получаем ID созданного заказа + from database import get_db + with get_db() as conn: + order_id = conn.execute('SELECT MAX(id) as id FROM orders').fetchone()['id'] + + # Проверяем, что на странице редактирования нет кнопки удаления + response = self.app.get(f'/edit_order/{order_id}') + self.assertEqual(response.status_code, 200) + + # Ищем текст, который указывает на отсутствие кнопки удаления + html_content = response.data.decode('utf-8') + + # Проверяем, что нет HTML-элемента с текстом "Удалить заказ" + self.assertNotIn('>Удалить заказ<', html_content) + + # Проверяем, что нет кнопки с классом btn-delete в HTML (не в CSS) + import re + # Ищем теги с классом btn-delete (например,