commit 6b1aeb9594cc07b2823c8682ef18c0dfd2277352 Author: Not Huts Date: Tue Apr 21 09:40:32 2026 +0500 Add files via upload diff --git a/README.md b/README.md new file mode 100644 index 0000000..619899d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +умма заказов Скидка +от 10 000 ₽ 5% +от 20 000 ₽ 10% +от 50 000 ₽ 20% \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..5db317c --- /dev/null +++ b/app.py @@ -0,0 +1,432 @@ +# -*- coding: utf-8 -*- +import os +from datetime import datetime +from flask import Flask, render_template, request, redirect, url_for, flash +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +import pandas as pd + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'mint-warehouse-secret-key-2025' +BASE_DIR = os.path.abspath(os.path.dirname(__file__)) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(BASE_DIR, 'instance', 'warehouse.db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db = SQLAlchemy(app) +migrate = Migrate(app, db) + +# ========== МОДЕЛИ ========== + +class Partner(db.Model): + __tablename__ = 'partners' + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), nullable=False) + short_name = db.Column(db.String(100)) + partner_type = db.Column(db.String(50)) + director = db.Column(db.String(255)) + phone = db.Column(db.String(50)) + discount = db.Column(db.Numeric(5, 2), default=0.00) + total_orders_sum = db.Column(db.Numeric(12, 2), default=0.00) + actual_address = db.Column(db.Text) + legal_address = db.Column(db.Text) + + orders = db.relationship('Order', backref='partner', lazy=True) + + # ДОБАВЬТЕ ЭТОТ МЕТОД + def calculate_discount(self): + """Расчёт скидки на основе суммы заказов""" + total = float(self.total_orders_sum) if self.total_orders_sum else 0 + if total >= 50000: + return 20 + elif total >= 20000: + return 10 + elif total >= 10000: + return 5 + else: + return 0 + + def update_discount(self): + """Обновить скидку партнёра""" + self.discount = self.calculate_discount() + + def __repr__(self): + return f'' +class ProductCategory(db.Model): + __tablename__ = 'product_categories' + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), nullable=False, unique=True) + description = db.Column(db.Text) + + products = db.relationship('Product', backref='category', lazy=True) + + def __repr__(self): + return f'' + +class Product(db.Model): + __tablename__ = 'products' + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True) + article = db.Column(db.String(50), unique=True, nullable=False) + name = db.Column(db.String(255), nullable=False) + commercial_name = db.Column(db.String(255)) + unit = db.Column(db.String(20), default='шт') + base_price = db.Column(db.Numeric(10, 2), nullable=False) + excise_tax = db.Column(db.Numeric(10, 2), default=0.00) + license_fee = db.Column(db.Numeric(10, 2), default=0.00) + is_alcohol = db.Column(db.Boolean, default=False) + alcohol_content = db.Column(db.Numeric(5, 2), nullable=True) + volume = db.Column(db.Numeric(10, 3), nullable=True) + category_id = db.Column(db.Integer, db.ForeignKey('product_categories.id'), nullable=False) + + order_items = db.relationship('OrderItem', backref='product', lazy=True) + + def __repr__(self): + return f'' + +class Order(db.Model): + __tablename__ = 'orders' + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True) + order_number = db.Column(db.String(50), unique=True, nullable=False) + date_created = db.Column(db.DateTime, default=datetime.now) + partner_id = db.Column(db.Integer, db.ForeignKey('partners.id'), nullable=False) + + items = db.relationship('OrderItem', backref='order', lazy=True, cascade='all, delete-orphan') + + def calculate_total(self): + total = 0 + for item in self.items: + total += item.calculate_subtotal() + return round(total, 2) + + def __repr__(self): + return f'' +class OrderItem(db.Model): + __tablename__ = 'order_items' + __table_args__ = {'extend_existing': True} + + id = db.Column(db.Integer, primary_key=True) + quantity = db.Column(db.Numeric(10, 3), nullable=False) + price_per_unit = db.Column(db.Numeric(10, 2), nullable=False) + order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False) + product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) + + def calculate_subtotal(self): + base = float(self.price_per_unit) * float(self.quantity) + discount = float(self.order.partner.discount) if self.order and self.order.partner else 0 + discounted_base = base * (1 - discount / 100) + excise = float(self.product.excise_tax) * float(self.quantity) + license = float(self.product.license_fee) * float(self.quantity) + return round(discounted_base + excise + license, 2) + + def __repr__(self): + return f'' + def calculate_subtotal(self): + base = float(self.price_per_unit) * float(self.quantity) + # Скидка берётся из партнёра (уже накопительная) + discount = float(self.order.partner.discount) if self.order and self.order.partner else 0 + discounted_base = base * (1 - discount / 100) + excise = float(self.product.excise_tax) * float(self.quantity) + license = float(self.product.license_fee) * float(self.quantity) + return round(discounted_base + excise + license, 2) +# ========== МАРШРУТЫ ========== + +@app.route('/') +def index(): + orders = Order.query.order_by(Order.date_created.desc()).all() + return render_template('index.html', orders=orders) + +@app.route('/products') +def products(): + products = Product.query.all() + categories = ProductCategory.query.all() + return render_template('products.html', products=products, categories=categories) + +@app.route('/product/new', methods=['GET', 'POST']) +def new_product(): + if request.method == 'POST': + try: + product = Product( + article=request.form['article'], + name=request.form['name'], + commercial_name=request.form.get('commercial_name', ''), + unit=request.form.get('unit', 'шт'), + base_price=float(request.form['base_price']), + excise_tax=float(request.form.get('excise_tax', 0)), + license_fee=float(request.form.get('license_fee', 0)), + is_alcohol='is_alcohol' in request.form, + alcohol_content=float(request.form['alcohol_content']) if request.form.get('alcohol_content') else None, + volume=float(request.form['volume']) if request.form.get('volume') else None, + category_id=int(request.form['category_id']) + ) + db.session.add(product) + db.session.commit() + flash(f'✅ Товар "{product.name}" добавлен!', 'success') + return redirect(url_for('products')) + except Exception as e: + flash(f'❌ Ошибка: {str(e)}', 'error') + db.session.rollback() + + categories = ProductCategory.query.all() + return render_template('product_form.html', product=None, categories=categories) + +@app.route('/product/edit/', methods=['GET', 'POST']) +def edit_product(id): + product = Product.query.get_or_404(id) + + if request.method == 'POST': + try: + product.article = request.form['article'] + product.name = request.form['name'] + product.commercial_name = request.form.get('commercial_name', '') + product.unit = request.form.get('unit', 'шт') + product.base_price = float(request.form['base_price']) + product.excise_tax = float(request.form.get('excise_tax', 0)) + product.license_fee = float(request.form.get('license_fee', 0)) + product.is_alcohol = 'is_alcohol' in request.form + product.alcohol_content = float(request.form['alcohol_content']) if request.form.get('alcohol_content') else None + product.volume = float(request.form['volume']) if request.form.get('volume') else None + product.category_id = int(request.form['category_id']) + + db.session.commit() + flash(f'✅ Товар "{product.name}" обновлен!', 'success') + return redirect(url_for('products')) + except Exception as e: + flash(f'❌ Ошибка: {str(e)}', 'error') + db.session.rollback() + + categories = ProductCategory.query.all() + return render_template('product_form.html', product=product, categories=categories) + +@app.route('/product/delete/') +def delete_product(id): + product = Product.query.get_or_404(id) + + if product.order_items: + flash(f'❌ Нельзя удалить товар "{product.name}", он используется в заявках!', 'error') + return redirect(url_for('products')) + + db.session.delete(product) + db.session.commit() + flash(f'✅ Товар "{product.name}" удален!', 'success') + return redirect(url_for('products')) + +@app.route('/partners') +def partners(): + partners = Partner.query.all() + return render_template('partners.html', partners=partners) + +@app.route('/partner/new', methods=['GET', 'POST']) +def new_partner(): + if request.method == 'POST': + try: + partner = Partner( + name=request.form['name'], + short_name=request.form.get('short_name', ''), + partner_type=request.form.get('partner_type', ''), + director=request.form.get('director', ''), + phone=request.form.get('phone', ''), + discount=float(request.form.get('discount', 0)), + actual_address=request.form.get('actual_address', ''), + legal_address=request.form.get('legal_address', '') + ) + db.session.add(partner) + db.session.commit() + flash(f'✅ Партнер "{partner.name}" добавлен!', 'success') + return redirect(url_for('partners')) + except Exception as e: + flash(f'❌ Ошибка: {str(e)}', 'error') + db.session.rollback() + + return render_template('partner_form.html', partner=None) + +@app.route('/partner/edit/', methods=['GET', 'POST']) +def edit_partner(id): + partner = Partner.query.get_or_404(id) + + if request.method == 'POST': + try: + partner.name = request.form['name'] + partner.short_name = request.form.get('short_name', '') + partner.partner_type = request.form.get('partner_type', '') + partner.director = request.form.get('director', '') + partner.phone = request.form.get('phone', '') + partner.discount = float(request.form.get('discount', 0)) + partner.actual_address = request.form.get('actual_address', '') + partner.legal_address = request.form.get('legal_address', '') + + db.session.commit() + flash(f'✅ Партнер "{partner.name}" обновлен!', 'success') + return redirect(url_for('partners')) + except Exception as e: + flash(f'❌ Ошибка: {str(e)}', 'error') + db.session.rollback() + + return render_template('partner_form.html', partner=partner) + +@app.route('/partner/delete/') +def delete_partner(id): + partner = Partner.query.get_or_404(id) + + if partner.orders: + flash(f'❌ Нельзя удалить партнера "{partner.name}", у него есть заявки!', 'error') + return redirect(url_for('partners')) + + db.session.delete(partner) + db.session.commit() + flash(f'✅ Партнер "{partner.name}" удален!', 'success') + return redirect(url_for('partners')) + +@app.route('/partner/') +def partner_detail(id): + partner = Partner.query.get_or_404(id) + orders = Order.query.filter_by(partner_id=id).order_by(Order.date_created.desc()).all() + return render_template('partner_detail.html', partner=partner, orders=orders) + +@app.route('/categories') +def categories(): + categories = ProductCategory.query.all() + return render_template('categories.html', categories=categories) + +@app.route('/category/new', methods=['GET', 'POST']) +def new_category(): + if request.method == 'POST': + try: + category = ProductCategory( + name=request.form['name'], + description=request.form.get('description', '') + ) + db.session.add(category) + db.session.commit() + flash('✅ Категория добавлена!', 'success') + return redirect(url_for('categories')) + except Exception as e: + flash(f'❌ Ошибка: {str(e)}', 'error') + db.session.rollback() + + return render_template('category_form.html', category=None) + +@app.route('/category/edit/', methods=['GET', 'POST']) +def edit_category(id): + category = ProductCategory.query.get_or_404(id) + + if request.method == 'POST': + try: + category.name = request.form['name'] + category.description = request.form.get('description', '') + db.session.commit() + flash('✅ Категория обновлена!', 'success') + return redirect(url_for('categories')) + except Exception as e: + flash(f'❌ Ошибка: {str(e)}', 'error') + db.session.rollback() + + return render_template('category_form.html', category=category) + +@app.route('/category/delete/') +def delete_category(id): + category = ProductCategory.query.get_or_404(id) + + if category.products: + flash('❌ Нельзя удалить категорию, в которой есть товары!', 'error') + return redirect(url_for('categories')) + + db.session.delete(category) + db.session.commit() + flash('✅ Категория удалена!', 'success') + return redirect(url_for('categories')) + +@app.route('/category/') +def category_detail(id): + category = ProductCategory.query.get_or_404(id) + products = Product.query.filter_by(category_id=id).all() + return render_template('category_detail.html', category=category, products=products) + +@app.route('/order/new', methods=['GET', 'POST']) +def new_order(): + if request.method == 'POST': + try: + # Генерация номера заявки + last_order = Order.query.order_by(Order.id.desc()).first() + if last_order: + last_num = int(last_order.order_number.split('-')[-1]) + new_num = last_num + 1 + else: + new_num = 1 + + order_number = f"ZAK-{datetime.now().strftime('%Y%m')}-{new_num:04d}" + + # Создаём заявку + order = Order( + order_number=order_number, + partner_id=int(request.form['partner_id']) + ) + db.session.add(order) + db.session.flush() + + # Добавляем позиции + product_ids = request.form.getlist('product_id[]') + quantities = request.form.getlist('quantity[]') + prices = request.form.getlist('price[]') + + for i in range(len(product_ids)): + if product_ids[i] and quantities[i] and float(quantities[i]) > 0: + item = OrderItem( + order_id=order.id, + product_id=int(product_ids[i]), + quantity=float(quantities[i]), + price_per_unit=float(prices[i]) + ) + db.session.add(item) + + # Сохраняем заявку + db.session.commit() + + # ========== ОБНОВЛЯЕМ СУММУ ЗАКАЗОВ ПАРТНЁРА ========== + partner = Partner.query.get(order.partner_id) + if partner: + # Получаем все заявки партнёра + all_orders = Order.query.filter_by(partner_id=partner.id).all() + total_sum = 0 + for o in all_orders: + total_sum += o.calculate_total() + + # Обновляем сумму и скидку + partner.total_orders_sum = total_sum + partner.discount = partner.calculate_discount() + db.session.commit() + + print(f"📊 Партнёр {partner.name}: сумма заказов = {total_sum:.2f} ₽, скидка = {partner.discount}%") + + flash(f'✅ Заявка {order_number} создана!', 'success') + return redirect(url_for('index')) + + except Exception as e: + flash(f'❌ Ошибка: {str(e)}', 'error') + db.session.rollback() + + partners = Partner.query.all() + products = Product.query.all() + return render_template('order_form.html', partners=partners, products=products, order=None) +@app.route('/order/') +def order_detail(id): + order = Order.query.get_or_404(id) + return render_template('order_detail.html', order=order) + +@app.route('/order/delete/') +def delete_order(id): + order = Order.query.get_or_404(id) + db.session.delete(order) + db.session.commit() + flash('✅ Заявка удалена!', 'success') + return redirect(url_for('index')) + +if __name__ == '__main__': + print("🚀 Запуск АлкоСклад...") + app.run(debug=True, host='0.0.0.0') \ No newline at end of file diff --git a/data/categories.xlsx b/data/categories.xlsx new file mode 100644 index 0000000..824cafb Binary files /dev/null and b/data/categories.xlsx differ diff --git a/data/partners.xlsx b/data/partners.xlsx new file mode 100644 index 0000000..06d425f Binary files /dev/null and b/data/partners.xlsx differ diff --git a/data/products.xlsx b/data/products.xlsx new file mode 100644 index 0000000..b669d7b Binary files /dev/null and b/data/products.xlsx differ diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..b2abb09 --- /dev/null +++ b/init_db.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Скрипт для создания базы данных и загрузки начальных данных из Excel +Запуск: python init_db.py +""" + +import os +import pandas as pd +from app import app, db +from app import Partner, ProductCategory, Product, Order, OrderItem + +with app.app_context(): + print("=" * 50) + print("ИНИЦИАЛИЗАЦИЯ БАЗЫ ДАННЫХ") + print("=" * 50) + + # Создаём папку instance, если её нет + if not os.path.exists('instance'): + os.makedirs('instance') + print("✅ Папка instance создана") + + # Создаём таблицы + print("\n1. Создание таблиц...") + db.create_all() + print(" ✅ Таблицы созданы") + + # Проверяем, есть ли уже данные + if ProductCategory.query.count() > 0: + print("\n⚠️ Таблицы уже содержат данные!") + response = input("Удалить все данные и загрузить заново? (y/n): ") + if response.lower() != 'y': + print("❌ Операция отменена") + exit() + + # Очищаем таблицы + print("\n2. Очистка существующих данных...") + OrderItem.query.delete() + Order.query.delete() + Product.query.delete() + Partner.query.delete() + ProductCategory.query.delete() + db.session.commit() + print(" ✅ Данные очищены") + + # Загружаем категории + print("\n3. Загрузка категорий...") + try: + df = pd.read_excel('data/categories.xlsx') + for _, row in df.iterrows(): + cat = ProductCategory( + id=int(row['id']), + name=row['name'], + description=row.get('description', '') + ) + db.session.add(cat) + db.session.commit() + print(f" ✅ Добавлено {len(df)} категорий") + except Exception as e: + print(f" ❌ Ошибка: {e}") + + # Загружаем партнёров + print("\n4. Загрузка партнёров...") + try: + df = pd.read_excel('data/partners.xlsx') + for _, row in df.iterrows(): + partner = Partner( + id=int(row['id']), + name=row['name'], + short_name=row.get('short_name', ''), + partner_type=row.get('partner_type', ''), + director=row.get('director', ''), + phone=row.get('phone', ''), + discount=float(row.get('discount', 0)), + total_orders_sum=0, # Начальная сумма заказов + actual_address=row.get('actual_address', ''), + legal_address=row.get('legal_address', '') + ) + db.session.add(partner) + db.session.commit() + print(f" ✅ Добавлено {len(df)} партнёров") + except Exception as e: + print(f" ❌ Ошибка: {e}") + + # Загружаем товары + print("\n5. Загрузка товаров...") + try: + df = pd.read_excel('data/products.xlsx') + # Преобразуем числовые поля + df['alcohol_content'] = pd.to_numeric(df['alcohol_content'], errors='coerce') + df['volume'] = pd.to_numeric(df['volume'], errors='coerce') + + for _, row in df.iterrows(): + product = Product( + id=int(row['id']), + article=row['article'], + name=row['name'], + commercial_name=row.get('commercial_name', ''), + unit=row.get('unit', 'шт'), + base_price=float(row['base_price']), + excise_tax=float(row.get('excise_tax', 0)), + license_fee=float(row.get('license_fee', 0)), + is_alcohol=bool(int(row.get('is_alcohol', 0))), + alcohol_content=float(row['alcohol_content']) if pd.notna(row['alcohol_content']) else None, + volume=float(row['volume']) if pd.notna(row['volume']) else None, + category_id=int(row['category_id']) + ) + db.session.add(product) + db.session.commit() + print(f" ✅ Добавлено {len(df)} товаров") + except Exception as e: + print(f" ❌ Ошибка: {e}") + + # Показываем статистику + print("\n" + "=" * 50) + print("СТАТИСТИКА") + print("=" * 50) + print(f"Категорий: {ProductCategory.query.count()}") + print(f"Партнёров: {Partner.query.count()}") + print(f"Товаров: {Product.query.count()}") + print(f"Заявок: {Order.query.count()}") + + print("\n✅ База данных успешно инициализирована!") \ No newline at end of file diff --git a/instance/warehouse.db b/instance/warehouse.db new file mode 100644 index 0000000..6204cc1 Binary files /dev/null and b/instance/warehouse.db differ diff --git a/reset_db.py b/reset_db.py new file mode 100644 index 0000000..f3e555f --- /dev/null +++ b/reset_db.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from app import app, db +from app import Partner, ProductCategory, Product, Order, OrderItem + +with app.app_context(): + print("=== ПОЛНЫЙ СБРОС БАЗЫ ДАННЫХ ===\n") + + print("1. Удаление таблиц...") + db.drop_all() + print(" ✅ Таблицы удалены") + + print("\n2. Создание таблиц...") + db.create_all() + print(" ✅ Таблицы созданы") + + from sqlalchemy import inspect + inspector = inspect(db.engine) + tables = inspector.get_table_names() + print(f"\n📋 Список таблиц: {', '.join(tables)}") + + print("\n3. Загрузка начальных данных...") + import pandas as pd + + # Категории + try: + df_cat = pd.read_excel('data/categories.xlsx') + for _, row in df_cat.iterrows(): + cat = ProductCategory( + id=int(row['id']), + name=row['name'], + description=row.get('description', '') + ) + db.session.add(cat) + db.session.commit() + print(f" ✅ Добавлено {len(df_cat)} категорий") + except Exception as e: + print(f" ❌ Ошибка загрузки категорий: {e}") + + # Партнёры + try: + df_part = pd.read_excel('data/partners.xlsx') + for _, row in df_part.iterrows(): + partner = Partner( + id=int(row['id']), + name=row['name'], + short_name=row.get('short_name', ''), + partner_type=row.get('partner_type', ''), + director=row.get('director', ''), + phone=row.get('phone', ''), + discount=float(row.get('discount', 0)), + actual_address=row.get('actual_address', ''), + legal_address=row.get('legal_address', '') + ) + db.session.add(partner) + db.session.commit() + print(f" ✅ Добавлено {len(df_part)} партнёров") + except Exception as e: + print(f" ❌ Ошибка загрузки партнёров: {e}") + + # Товары + try: + df_prod = pd.read_excel('data/products.xlsx') + df_prod['alcohol_content'] = pd.to_numeric(df_prod['alcohol_content'], errors='coerce') + df_prod['volume'] = pd.to_numeric(df_prod['volume'], errors='coerce') + + for _, row in df_prod.iterrows(): + product = Product( + id=int(row['id']), + article=row['article'], + name=row['name'], + commercial_name=row.get('commercial_name', ''), + unit=row.get('unit', 'шт'), + base_price=float(row['base_price']), + excise_tax=float(row.get('excise_tax', 0)), + license_fee=float(row.get('license_fee', 0)), + is_alcohol=bool(int(row.get('is_alcohol', 0))), + alcohol_content=float(row['alcohol_content']) if pd.notna(row['alcohol_content']) else None, + volume=float(row['volume']) if pd.notna(row['volume']) else None, + category_id=int(row['category_id']) + ) + db.session.add(product) + db.session.commit() + print(f" ✅ Добавлено {len(df_prod)} товаров") + except Exception as e: + print(f" ❌ Ошибка загрузки товаров: {e}") + + print("\n=== ПРОВЕРКА ===") + print(f"Категорий: {ProductCategory.query.count()}") + print(f"Партнёров: {Partner.query.count()}") + print(f"Товаров: {Product.query.count()}") + print(f"Заявок: {Order.query.count()}") + + print("\n✅ База данных готова!") \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..9b7a26f --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,262 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: #F5FFFA; + padding: 20px; + color: #2F4F4F; + line-height: 1.6; +} + +.container { + max-width: 1200px; + margin: 0 auto; + background-color: #F0FFF0; + border: 2px solid #3CB371; + border-radius: 10px; + padding: 20px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px; + border-bottom: 2px solid #3CB371; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 15px; +} + +.logo h1 { + color: #3CB371; + margin: 0; + font-size: 1.8em; +} + +.nav-menu { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.btn { + background-color: #F5FFFA; + color: #2F4F4F; + padding: 8px 16px; + text-decoration: none; + border: 1px solid #3CB371; + border-radius: 5px; + transition: all 0.3s; + display: inline-block; +} + +.btn:hover { + background-color: #3CB371; + color: white; +} + +.btn-primary { + background-color: #3CB371; + color: white; + border: none; +} + +.btn-primary:hover { + background-color: #2E8B57; +} + +.btn-delete { + background-color: #ff6b6b; + border-color: #ff0000; + color: white; +} + +.btn-delete:hover { + background-color: #ff0000; +} + +.btn-small { + padding: 4px 8px; + font-size: 0.9em; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; + background-color: white; + border-radius: 8px; + overflow: hidden; +} + +th { + background-color: #3CB371; + color: white; + padding: 12px; + text-align: left; +} + +td { + padding: 12px; + border-bottom: 1px solid #F0FFF0; +} + +tr:hover { + background-color: #F5FFFA; +} + +.actions { + display: flex; + gap: 5px; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 10px; +} + +.form-container { + max-width: 600px; + margin: 0 auto; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #3CB371; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 8px; + border: 1px solid #3CB371; + border-radius: 5px; + font-family: inherit; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #2E8B57; + box-shadow: 0 0 5px rgba(60, 179, 113, 0.5); +} + +.form-actions { + display: flex; + gap: 10px; + margin-top: 20px; +} + +.alert { + padding: 12px; + margin: 20px 0; + border-radius: 5px; +} + +.alert-success { + background-color: #d4edda; + border: 1px solid #c3e6cb; + color: #155724; +} + +.alert-error { + background-color: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + background-color: white; + border: 2px dashed #3CB371; + border-radius: 8px; + margin: 30px 0; +} + +.empty-state p { + font-size: 1.2em; + color: #666; + margin-bottom: 20px; +} + +.footer { + text-align: center; + margin-top: 30px; + padding-top: 20px; + border-top: 2px solid #3CB371; + color: #666; +} + +.status { + display: inline-block; + padding: 4px 8px; + border-radius: 12px; + font-size: 0.85em; + font-weight: bold; +} + +.status-new { + background-color: #e3f2fd; + color: #1976d2; +} + +.status-processing { + background-color: #fff3e0; + color: #f57c00; +} + +.status-ready { + background-color: #e8f5e8; + color: #388e3c; +} + +.status-shipped { + background-color: #e8eaf6; + color: #3f51b5; +} + +.status-cancelled { + background-color: #ffebee; + color: #d32f2f; +} + +@media (max-width: 768px) { + .header { + flex-direction: column; + text-align: center; + } + + .nav-menu { + justify-content: center; + } + + table { + display: block; + overflow-x: auto; + } + + .actions { + flex-direction: column; + } + + .actions .btn { + width: 100%; + } +} \ No newline at end of file diff --git a/static/images/5294256562463512548.jpg b/static/images/5294256562463512548.jpg new file mode 100644 index 0000000..ccc6fe7 Binary files /dev/null and b/static/images/5294256562463512548.jpg differ diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..c659c2e --- /dev/null +++ b/templates/base.html @@ -0,0 +1,47 @@ + + + + + + {% block title %}АлкоСклад{% endblock %} + + + + +
+
+ + +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+ {% block content %}{% endblock %} +
+ +
+

🍷 АлкоСклад - Система управления заявками © 2026

+
+
+ + \ No newline at end of file diff --git a/templates/categories.html b/templates/categories.html new file mode 100644 index 0000000..fbd6068 --- /dev/null +++ b/templates/categories.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}Категории{% endblock %} + +{% block content %} + + + + + + + + + + + + + + {% for category in categories %} + + + + + + + + {% endfor %} + +
IDНазваниеОписаниеТоваровДействия
{{ category.id }}{{ category.name }}{{ category.description or '—' }}{{ category.products|length }} + 👁️ + ✏️ + 🗑️ +
+{% endblock %} \ No newline at end of file diff --git a/templates/category_detail.html b/templates/category_detail.html new file mode 100644 index 0000000..a7f4290 --- /dev/null +++ b/templates/category_detail.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %}{{ category.name }}{% endblock %} + +{% block content %} +
+

{{ category.name }}

+ ← К списку +
+ +
+

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

+

Описание: {{ category.description or '—' }}

+
+ +
+

Товары в категории

+ {% if products %} + + + + + + + + + + + {% for product in products %} + + + + + + + {% endfor %} + +
АртикулНаименованиеЦенаАлкоголь
{{ product.article }}{{ product.name }}{{ product.base_price }} ₽{% if product.is_alcohol %}✅{% else %}❌{% endif %}
+ {% else %} +

В категории нет товаров

+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/category_form.html b/templates/category_form.html new file mode 100644 index 0000000..bcd2b15 --- /dev/null +++ b/templates/category_form.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block title %}{{ 'Редактирование' if category else 'Добавление' }} категории{% endblock %} + +{% block content %} +
+

{{ '✏️ Редактирование категории' if category else '➕ Добавление новой категории' }}

+ +
+
+ + +
+ +
+ + +
+ +
+ + ↩️ Отмена +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..69c13f5 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block title %}Заявки{% endblock %} + +{% block content %} + + +{% if orders %} +
+ + + + + + + + + + + + {% for order in orders %} + + + + + + + + {% endfor %} + +
НомерДатаПартнерСуммаПозицийДействия
{{ order.order_number|default('?') }}\\ + {{ order.date_created.strftime('%d.%m.%Y %H:%M') if order.date_created else '—' }} + {% if order.partner %} + {{ order.partner.name|default('Без имени') }} + {% else %} + Партнёр удалён + {% endif %} + {{ order.calculate_total()|default(0) }} ₽{{ order.items|length|default(0) }} + 👁️ + 🗑️ +
+
+{% else %} +
+

📭 Нет заявок

+ ➕ Создать первую заявку +
+{% endif %} +{% 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..37e6dfb --- /dev/null +++ b/templates/order_detail.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} + +{% block title %}Заявка {{ order.order_number|default('?') }}{% endblock %} + +{% block content %} +
+

Заявка {{ order.order_number|default('?') }}

+ ← К списку +
+ +
+

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

+ + + + + + + + + +
Дата создания:{{ order.date_created.strftime('%d.%m.%Y %H:%M') if order.date_created else '—' }}
Партнер: + {% if order.partner %} + + {{ order.partner.name|default('Партнёр удалён') }} + + {% else %} + Партнёр удалён (ID: {{ order.partner_id|default('?') }}) + {% endif %} +
+
+ +
+

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

+ + + + + + + + + + + + + + {% for item in order.items %} + + + + + + + + + + {% endfor %} + + + + + + + +
АртикулНаименованиеКоличествоЦенаАкцизСборСумма
{{ item.product.article|default('—') }}{{ item.product.name|default('—') }}{{ item.quantity|default(0) }} {{ item.product.unit|default('шт') }}{{ item.price_per_unit|default(0) }} ₽{{ item.product.excise_tax|default(0) }} ₽{{ item.product.license_fee|default(0) }} ₽{{ item.calculate_subtotal()|default(0) }} ₽
ИТОГО:{{ order.calculate_total()|default(0) }} ₽
+
+{% endblock %} \ No newline at end of file diff --git a/templates/order_form.html b/templates/order_form.html new file mode 100644 index 0000000..075a55a --- /dev/null +++ b/templates/order_form.html @@ -0,0 +1,353 @@ +{% extends "base.html" %} + +{% block title %}{{ 'Редактирование' if order else 'Новая' }} заявка{% endblock %} + +{% block content %} +
+

{{ '✏️ Редактирование заявки' if order else '➕ Новая заявка' }}

+ +
+
+ + +
+ +

Позиции заявки

+
+ {% if order %} + {% for item in order.items %} +
+ + + + +
+
+ + +
+ +
+ + {{ item.calculate_subtotal() }} ₽ +
+ +
+ Базовая: {{ item.product.base_price }} ₽ | + Акциз: {{ item.product.excise_tax }} ₽ | + Сбор: {{ item.product.license_fee }} ₽ + {% if order.partner and order.partner.discount > 0 %} + | Скидка: {{ order.partner.discount }}% + {% endif %} +
+
+
+ {% endfor %} + {% else %} +
+ + + + + + +
+
+ + +
+ +
+ + 0.00 ₽ +
+ +
+
+
+ {% endif %} +
+ + {% if not order %} + + {% endif %} + +
+
+

ИТОГО:

+

0.00 ₽

+
+
+ +
+ + ↩️ Отмена +
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/partner_detail.html b/templates/partner_detail.html new file mode 100644 index 0000000..2997d76 --- /dev/null +++ b/templates/partner_detail.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} + +{% block title %}{{ partner.name }}{% endblock %} + +{% block content %} +
+

{{ partner.name }}

+ ← К списку +
+ +
+

Информация о партнере

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Полное наименование:{{ partner.name }}
Сокращенное:{{ partner.short_name or '—' }}
Тип:{{ partner.partner_type or '—' }}
Руководитель:{{ partner.director or '—' }}
Телефон:{{ partner.phone or '—' }}
Фактический адрес:{{ partner.actual_address or '—' }}
Юридический адрес:{{ partner.legal_address or '—' }}
+
+ +
+

Заявки партнера

+ {% if orders %} + + + + + + + + + + + + {% for order in orders %} + + + + + + + + {% endfor %} + +
НомерДатаСтатусСуммаДействия
{{ order.order_number }}{{ order.date_created.strftime('%d.%m.%Y') }}{{ order.status }}{{ order.calculate_total() }} ₽ + 👁️ +
+ {% else %} +

У партнера нет заявок

+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/partner_form.html b/templates/partner_form.html new file mode 100644 index 0000000..5126c43 --- /dev/null +++ b/templates/partner_form.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} + +{% block title %}{{ 'Редактирование' if partner else 'Добавление' }} партнера{% endblock %} + +{% block content %} +
+

{{ '✏️ Редактирование партнера' if partner else '➕ Новый партнер' }}

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + Пример: +7(123)-456-78-90 +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + ↩️ Отмена +
+
+
+ + +{% endblock %} diff --git a/templates/partners.html b/templates/partners.html new file mode 100644 index 0000000..d4ca380 --- /dev/null +++ b/templates/partners.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block title %}Партнеры{% endblock %} + +{% block content %} + + + + + + + + + + + + + + + + + {% for partner in partners %} + {% if partner and partner.id %} + + + + + + + + + + + + {% endif %} + {% endfor %} + +
IDНаименованиеТипРуководительТелефонСумма заказовСкидкаЗаявокДействия
{{ partner.id|default('?') }}{{ partner.name|default('Без имени') }}{{ partner.partner_type|default('—') }}{{ partner.director|default('—') }}{{ partner.phone|default('—') }}{{ "%.2f"|format(partner.total_orders_sum|default(0)) }} ₽{{ partner.discount|default(0) }}%{{ partner.orders|length|default(0) }} + 👁️ + ✏️ + 🗑️ +
+{% endblock %} \ No newline at end of file diff --git a/templates/product_form.html b/templates/product_form.html new file mode 100644 index 0000000..52094a5 --- /dev/null +++ b/templates/product_form.html @@ -0,0 +1,98 @@ +{% extends "base.html" %} + +{% block title %}{{ 'Редактирование' if product else 'Добавление' }} товара{% endblock %} + +{% block content %} +
+

{{ '✏️ Редактирование товара' if product else '➕ Добавление нового товара' }}

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + ↩️ Отмена +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/products.html b/templates/products.html new file mode 100644 index 0000000..2324501 --- /dev/null +++ b/templates/products.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}Товары{% endblock %} + +{% block content %} + + +
+
+ + + + Сбросить +
+
+ + + + + + + + + + + + + + + + {% for product in products %} + + + + + + + + + + + {% endfor %} + +
АртикулНаименованиеКатегорияЦенаАкцизСборАлкогольДействия
{{ product.article }}{{ product.name }}{{ product.category.name }}{{ product.base_price }} ₽{{ product.excise_tax }} ₽{{ product.license_fee }} ₽{% if product.is_alcohol %}✅{% else %}❌{% endif %} + ✏️ + 🗑️ +
+{% endblock %} \ No newline at end of file