From 41ece8851b24c60292c9e1a5b23d6344eb23c816 Mon Sep 17 00:00:00 2001 From: Zver Date: Fri, 13 Mar 2026 11:08:33 +0500 Subject: [PATCH] 13.03.26 --- app.py | 722 +++++++++++++++++++++++++ data/Order_items_import.xlsx | Bin 0 -> 4926 bytes data/Orders_import.xlsx | Bin 0 -> 4922 bytes data/Partners_import.xlsx | Bin 0 -> 5019 bytes data/Product_types_import.xlsx | Bin 0 -> 4895 bytes data/Products_import.xlsx | Bin 0 -> 4969 bytes database.py | 221 ++++++++ import_data.py | 420 +++++++++++++++ init_db.py | 5 + partners.db | Bin 0 -> 40960 bytes requirements.txt | 3 + static/css/style.css | 681 +++++++++++++++++++++++ static/img/logo.png | Bin 0 -> 4488 bytes static/js/script.js | 193 +++++++ templates/add_order.html | 455 ++++++++++++++++ templates/add_partner.html | 221 ++++++++ templates/base.html | 52 ++ templates/edit_order.html | 412 ++++++++++++++ templates/edit_partner.html | 239 ++++++++ templates/import.html | 267 +++++++++ templates/index.html | 260 +++++++++ templates/manual_partner_test.html | 487 +++++++++++++++++ templates/order_detail.html | 171 ++++++ templates/orders.html | 228 ++++++++ templates/partners.html | 222 ++++++++ templates/products.html | 778 +++++++++++++++++++++++++++ templates/search_results.html | 131 +++++ templates/view_order.html | 211 ++++++++ tests/__init__.py | 0 tests/data/Order_items_import.xlsx | Bin 0 -> 4928 bytes tests/data/Orders_import.xlsx | Bin 0 -> 4947 bytes tests/data/Partners_import.xlsx | Bin 0 -> 5201 bytes tests/data/Product_types_import.xlsx | Bin 0 -> 4964 bytes tests/data/Products_import.xlsx | Bin 0 -> 5081 bytes tests/test_calculation.py | 60 +++ tests/test_database.py | 130 +++++ tests/test_integration.py | 272 ++++++++++ tests/test_orders.py | 230 ++++++++ tests/test_partners.py | 166 ++++++ tests/test_products.py | 162 ++++++ tests/tests.txt | 52 ++ 41 files changed, 7451 insertions(+) create mode 100644 app.py create mode 100644 data/Order_items_import.xlsx create mode 100644 data/Orders_import.xlsx create mode 100644 data/Partners_import.xlsx create mode 100644 data/Product_types_import.xlsx create mode 100644 data/Products_import.xlsx create mode 100644 database.py create mode 100644 import_data.py create mode 100644 init_db.py create mode 100644 partners.db create mode 100644 requirements.txt create mode 100644 static/css/style.css create mode 100644 static/img/logo.png create mode 100644 static/js/script.js create mode 100644 templates/add_order.html create mode 100644 templates/add_partner.html create mode 100644 templates/base.html create mode 100644 templates/edit_order.html create mode 100644 templates/edit_partner.html create mode 100644 templates/import.html create mode 100644 templates/index.html create mode 100644 templates/manual_partner_test.html create mode 100644 templates/order_detail.html create mode 100644 templates/orders.html create mode 100644 templates/partners.html create mode 100644 templates/products.html create mode 100644 templates/search_results.html create mode 100644 templates/view_order.html create mode 100644 tests/__init__.py create mode 100644 tests/data/Order_items_import.xlsx create mode 100644 tests/data/Orders_import.xlsx create mode 100644 tests/data/Partners_import.xlsx create mode 100644 tests/data/Product_types_import.xlsx create mode 100644 tests/data/Products_import.xlsx create mode 100644 tests/test_calculation.py create mode 100644 tests/test_database.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_orders.py create mode 100644 tests/test_partners.py create mode 100644 tests/test_products.py create mode 100644 tests/tests.txt 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 0000000000000000000000000000000000000000..d0c315d5480ec731e7c5e9f61d493e3f69836653 GIT binary patch literal 4926 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 literal 0 HcmV?d00001 diff --git a/data/Orders_import.xlsx b/data/Orders_import.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ded80596ac0420c0264f2a760cd91a113d041ccf GIT binary patch literal 4922 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 literal 0 HcmV?d00001 diff --git a/data/Partners_import.xlsx b/data/Partners_import.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..68a0a0e8ee5fa77832b0ab98c8fc7bb564c85dac GIT binary patch literal 5019 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 bcK#!m literal 0 HcmV?d00001 diff --git a/data/Product_types_import.xlsx b/data/Product_types_import.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..311933b2e3ae8487ca6ab033ee31e93227813604 GIT binary patch literal 4895 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 I{V%}(0l;U2M*si- literal 0 HcmV?d00001 diff --git a/data/Products_import.xlsx b/data/Products_import.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9c0919d998af4dd4fc6495c5d706baf3fdc0757d GIT binary patch literal 4969 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 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 0000000000000000000000000000000000000000..55b7fcf44556c3a6a805d3d43df36db060722fcc GIT binary patch literal 40960 zcmeI*Z)_7~90&0G)3v?1?wR6TFd2EqvaAg-+Kp{2F-q4RurRvObw*w_wLRgIt{v^o z$O{uT2qb8tL|^cRKfdsti3FUP;P9m(zPU!?D_>|d!5IHV3E`!`yZ*fnm~j!1d~a)e z_dIvcbI<4ZT+`k?w@n|I&<%~hmdnp34L%^W36do2;#?2}i5|`Ls7y^XJN>MW-4zti9ijDSw$ggmx2g5eQ!ljqs{BMrm>>WF2tWV=5O`<;$8(COqq|!? z)ng=I&1kuNTFWQsbHHq&ZnilRQ^RqU$HSu&DzD4n{$^e`>1jS5jjMaq7@vxb?+?cg z@_p(-9-fI$jz_7C{c1G6)v-mntlDkjc;GH+z9PPJB$OGxP?eFz=1dRv9u zX_3ttT7k#aR~mX!S}&w>^I0P?r{z;x)~NP?F?Dx%W+Kjm&T^cl)qQlDmXg;tJ>KW` zbaZx#C%4Jr37O>iT8NM}={8PFtI|Tx}wooYnZt;aFsEIOgvk9IW+iClIw= zPi1PWhJwKWpNWnin7LnQX7y~M5^ekVSmPL4z5W`9QKbpx20LR_hp~3RwtL(ikxo(2 zvuW*(!jTNU<0X>wM$TSNR3elJRpz{lyIppKPb5kaF87Rba|=z*2$yr$9pQf(tF)fi zQbsQCh^sT4+H6wKIP&jvfXyAwWouC%YJQ&yI4VTl;_A=cWCWC?1-;@fK}e~%14?rhH0&k%IkB6p3B zu<9&SJ9TMMrjHt4>*I!x9wPfqpkGW7fB*y_009U<00Izz00bZa0SG)10&Sv8=<%6n zipR~*%}eGL^OALeUu1s>^ot1s5P$##AOHafKmY;|fB*y_0D&h&fXO{d{r7(#yCF2O z^8!21zGq*vkJ$1P(oi%50uX=z1Rwwb2tWV=5P$##An?BtXc6U3v2ss@-0YG!iuV0d z1s7|QyG!$>`}RESFQHknGh1Y9qtfjS1-VU*whCsx^05OZBCb zuj}c)qe<(pI_x@R{l$a6RBkrZKd@tPXn5yHXrWv33NPtecJdk74!)=KQxz$ zr>ruI@0cs*1zK7x9y70-m$~__dCgp+j0JlBjGG@&-Z2_2zeGQ@aq(1Rt$q3VTE$z2 zxqoD2B*5uu8~qLq)27$$qN&z1RK_)XajAIHyjtDMpdosNnSH$>YUx??0_{U_q4*Z9 zdW8n-e@eT#!YTb)@ogHpzf6TaI9t!m>iJzZMSgzu4uuAX21bH|p-{*g|1Yzf0{em8 zWPd)A5TjBEKmY;|fB*y_009U<00Izz00jPpK&#v(R?jPhWXEDq^2uFF?fbud{hz%j zurJxi^nwWj5P$##AOHafKmY;|fB*y_0D*@i@Vx96l)whryg--v>AF7+bD-e|!C*){ zdGR@k@_O2p(s%+IpYTQT*c4?Ai|P}0TTou009U<00Izz00bZa0SG_<0*{YCi-*4Ri=yn3CAv32anq8bNV0g_+hk=Z biYzr-rv(&Ik|avC&Iiy6G-sU$u;=~;2%C%k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dfc45473abfc8508389af725284249eba8d64700 GIT binary patch literal 4488 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 a)c*l&;0u)Eg-QSb0000 { + 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 0000000000000000000000000000000000000000..5cc8a50b6f3817fe4e4692558ab10622eb4a2258 GIT binary patch literal 4928 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

4M1N!mWy2| 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 G2KW!ytQOb+ literal 0 HcmV?d00001 diff --git a/tests/data/Product_types_import.xlsx b/tests/data/Product_types_import.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..05d914e5267b2122a66480988b30dbddc69a3578 GIT binary patch literal 4964 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 YwAC??9Torp8+l73BRJ8I4iw=300{S`YXATM literal 0 HcmV?d00001 diff --git a/tests/data/Products_import.xlsx b/tests/data/Products_import.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d6792e615385d1aeca742a069de194c4ed4ddba2 GIT binary patch literal 5081 zcmZ`-1yodP*B(k>Xoiv$DUmMe9BD+54uLBmHFSeW4MW;% zF_(?2wT_3Yo2S5EZf^X3&X9ZXdqh2g#Mf539{4oBh+!@!y(=4&*8%7Ej%ea`hMw;4 z;mW)C`wub{@<-p~Y|2zV5gs5>m}Wbb8y+f%DcA_C5n2q5tD`3?W?{zZEp(m< zi4Y=|7+Gts-I?T9vGazfEBU^b(&g-3GZvMq<6@~?U<8tH1#rm4f;1&Fcym|q{p0stuf?Si$dhwV=vN|L&@dj*LjEp3P1cz@_2 z)y)_t=Sob>#9gD|SfW9Q>OPwMx~D!o9lFvlbRi$ylxB!u5Y$(;jid59)39jwl-+B# zmQ9x&pw5DLrT@4PzPhAZc1wO8D{Nx_bBK=76g^|Ia151J&rMzxA4B>#w|Z5-JPt*K zFiCyk#uAV;H|g1)$S3i9ooM}{Xj;*a#k+svzYKldOoka%5pLliG@aD*$S*MLEQ7oo+f;77{O>jE-^?$Hm>8Y3m1l(^nYt)OtIN^z8>fh1YAM$* zOBcO<6oWKR&$G{P{|w9v^BgwvYXCqF7XSdl1jY{{;AL;?Z2M~!`Wc!%Q!~#w396_2 zuyH>-lrvHPozSKRkf(#se8UUqo;FJag>gK@-zEtf+@*Sld^nwCFZo_uwhS)s&W|x= z)vav_f8ijdQ&-Cy4Vx??->HZ7<86zu zKt_t3tx*j}Xvn*>xg|MU_>L5laHxwQgYk{vG1syUZPTb#{hq$Jj&|~fP6LPuwUDs) zL8*9c>Zqa*)C9sL(o(s`V?Iw%eS7(FLvvoD&h#tmhkg=G!xOP?jJPYh?Rh=9$B?dK zMZ$(6hjE)()LczcX+^x={SV)QmUpx5bAa(?_dhuKE`CUe?ynyABk4T6|E#wry1#PV zYuAC3>%7LPc!$PbNo9REfReg#Q{lr6fU%p;^vDEGx&`;CcPjlyruIce^8@3rlF<|w z{qJz69VV`)i>i3!CgPC-#QP9hI!|DVO))Dy$rI*@({S!a9eRSS!O#=cFxGbqu=A6Q z_V>ji0l7g^V@|2T4wYDCd+piqlrzumjr_8dnR`lOZXLXYhdF`-a;LFD&_{wj(UP*8Ix!QgUy)VE64ELo!hWA!=ccXAc_ zwH(h+o(RYf{1O!e%AjMRD8rZZY)Qq%tnWvS(AEE7I9nncaedPVk@v)G}7+kJ%FHf9`z!}=d=#bYS*lo(ug^iDL=W;S81 zw_0S(Hg;eIUUIx}w2b9e!zhsmxYcQ+_|yjV>^5-|NmdA4v8%WPPXq4S zdM%VbO=Q@@t?iPp0&IosN{^QLhzWVkqD&~)?mgU_+RDzU3ApKVn&I1Rb#(6bUX^T9 zddu?oA-Lu#YH|J4B}H0IE64vG1(ugGHd`9wH5yvs4V|yaiPu0_qp9!j9$<&rlrn~7 zo)Wz|B5viT9;=p<3|8h1kJPr9;Gm?6sU^l^rS(|f-%oM$jhi{;4Rwa<3)@W54uPY) zWko!|v-%v!nmJCQi1{`B(RTVyPAkpc0GC`ouFH9gN!bQkSruf_ixuYjXP(&sEj`Lr zm&Vh~XyIi^w|S{09E(pNUU2c#?x2{M5Q@r2oo%rDXpS7|hQPzn8`)>m^Tnve72+q8 zm8K;&JQVVp_)l1puUW^L9gA8%r+QRfT36jaCq(QYVvHm%cr3B}OgW2QWGc2wG9Kuv zIHj;DKR-!gk0KFeOv1AedeL;JXhRz)vv9FbQrpv)u(}u9h4p6x=_+MblEcP`0!$O3 z`_({vT|JyU?QLznJO%!~{oO_4(?(r#1jz?1@eTn(wTX8rVkaRF8lPz(IGvyK2#oL> zzAf!&|%Zb9{E#1LjRYLn^*#nXN?W=J|ytDfTsW_g88ICW2o+PoU%ovfxt z!&Wdh!>lobc-FKXQx8dsiEb&kn&4qNzpvp=DBH%CP3oyQQFz}2l9CG5zFkuEmW!He zCz9^fCod*o8!4XiopW72ki3d!J&PGiCA0*HE<(>eN%1`zO6uAaK)=kJIkH%~y{l*; zw)~^bO7jj%rVEDm_^&`+qmS}Y;iT?Xckvmb!ug`Y?QSfI75CW_zgzGiAaz#MU>A^)feUHD?-p63qq$5AKQ5T|_oLWW7qzDapu!foN6UXluX zB7Q9JcxVJCgK%j|UZ<;IKFCxWZzOeS{-d79g~S*$Q+I`r>HQbv zRgh8hbG(pUK%>01mRACE+2V5!G|r!#o4`9%d4S=ZDi8o5`NcU;FMo)w=TEt^XgD7; zFGzk)y-rr1l9|=Q?qANTV?W&U6>Pa_tc}~&^16fl{M3P$_q&;5xZic zu?k7VU!UB%K0oa=+kvTz^wf zYul@SB7E-l441TseY3WmL$b4BHDYF5CLXIRojb2XxK*2nQ`uSBd?YoojLs4|IHqO| z+M04J)GNQFrX5kuC{Aci9q*5{rx}K-?t_T91#Z(g5JL=^JQK{`oM^L0YzIObEVXlu z>YBfZT0YTorCVJcp6tJ~u++!^&Q9B?W{af$0T#^J+16#)io01!0dW25+uB%*D;(fMW>x8kam&K_w3yWid-gFm!{J zd3-J`-|GDNSYr0?y5i7tb zZ7sE4^QQlY;f|&ghH^x!2Us?9EvOjWlb^4iS z@rj6^J0|7ek|F$YmoRm$U5m^Rbpmp5Al%fqF}*Ks-K7@BrOvIj7QrVRo)#Da$_>oD)@tv2# zv7~%?1^fmD!pjpoNp$dyyV>{N*=W7u?6o4|H9k)gM|^5uLKjCZy?HlMl0}U*dm~Vt zAsQC`o(;NLup99eL2#f!BqMmj)E3kOr9&$xc*WE`);7CkP{>JEP_x_p9CutA%~vcOGh3*@p*V+1&}MABdK&oJY{~RQ~VRZy};LKuXAfi$LAxWKZ7f>yWUof z3F;3_fa!k)_ZN}=9pb-9)tDqp8ZJm20i)am(3MXXV3+b&QPW0+p(#S|)>ZcGF?0je znA!wQ;OSl8Z0rO?V*;ghXQU~S_MAhu17DZ{#nOz_%G0Yk1Apyl>l59Z1U*Oe`r66~nZ}{=tyYi0?*6;x zZc2I4^--+HJgfTO142^cA|X7WH);b8h4t7w_E^Ki^mc8;-p*9pFHs4a8gYch{ntWr z_*4<^s6`JG|2kV5b+JlpRoj?!22y-%#0}och3r(u4k{%MnoDj#ZT3ELL6_V~P7NjQ ze`hQ9Ilz5aJHmqW&F4BdqxUB(INuMPhLxoB-zSM$m4(S;bQTsi72to5VKA)u`v}2A z`2P=OuA;9_Kz?BXfDo+Sf204Ihg^kUofG_qFJM0ZA5(*?2CnMFe+)E*G7Удалить заказ<', html_content) + + # Проверяем, что нет кнопки с классом btn-delete в HTML (не в CSS) + import re + # Ищем теги с классом btn-delete (например,