Add files via upload

This commit is contained in:
Роганов Руслан Николаевич 2026-04-21 09:40:32 +05:00 committed by GitHub
commit 6b1aeb9594
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1921 additions and 0 deletions

4
README.md Normal file
View file

@ -0,0 +1,4 @@
умма заказов Скидка
от 10 000 ₽ 5%
от 20 000 ₽ 10%
от 50 000 ₽ 20%

432
app.py Normal file
View file

@ -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'<Partner {self.name}>'
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'<Category {self.name}>'
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'<Product {self.article}: {self.name}>'
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'<Order {self.order_number}>'
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'<OrderItem {self.id}>'
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/<int:id>', 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/<int:id>')
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/<int:id>', 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/<int:id>')
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/<int:id>')
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/<int:id>', 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/<int:id>')
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/<int:id>')
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/<int:id>')
def order_detail(id):
order = Order.query.get_or_404(id)
return render_template('order_detail.html', order=order)
@app.route('/order/delete/<int:id>')
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')

BIN
data/categories.xlsx Normal file

Binary file not shown.

BIN
data/partners.xlsx Normal file

Binary file not shown.

BIN
data/products.xlsx Normal file

Binary file not shown.

123
init_db.py Normal file
View file

@ -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✅ База данных успешно инициализирована!")

BIN
instance/warehouse.db Normal file

Binary file not shown.

94
reset_db.py Normal file
View file

@ -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✅ База данных готова!")

262
static/css/style.css Normal file
View file

@ -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%;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

47
templates/base.html Normal file
View file

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}АлкоСклад{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
@import url('https://fonts.googleapis.com/css2?family=Corbel&display=swap');
body {
font-family: 'Corbel', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<div class="logo">
<h1>🍷 АлкоСклад</h1>
</div>
<nav class="nav-menu">
<a href="{{ url_for('index') }}" class="btn">📋 Заявки</a>
<a href="{{ url_for('products') }}" class="btn">📦 Товары</a>
<a href="{{ url_for('partners') }}" class="btn">🤝 Партнеры</a>
<a href="{{ url_for('categories') }}" class="btn">📊 Категории</a>
<a href="{{ url_for('new_order') }}" class="btn btn-primary">➕ Новая заявка</a>
</nav>
</header>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<main class="content">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<p>🍷 АлкоСклад - Система управления заявками © 2026</p>
</footer>
</div>
</body>
</html>

37
templates/categories.html Normal file
View file

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}Категории{% endblock %}
{% block content %}
<div class="page-header">
<h2>📊 Категории товаров</h2>
<a href="{{ url_for('new_category') }}" class="btn btn-primary">➕ Новая категория</a>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Описание</th>
<th>Товаров</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr>
<td>{{ category.id }}</td>
<td><strong>{{ category.name }}</strong></td>
<td>{{ category.description or '—' }}</td>
<td>{{ category.products|length }}</td>
<td class="actions">
<a href="{{ url_for('category_detail', id=category.id) }}" class="btn btn-small" title="Просмотр">👁️</a>
<a href="{{ url_for('edit_category', id=category.id) }}" class="btn btn-small" title="Редактировать">✏️</a>
<a href="{{ url_for('delete_category', id=category.id) }}" class="btn btn-small btn-delete" onclick="return confirm('Удалить категорию {{ category.name }}?')" title="Удалить">🗑️</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}{{ category.name }}{% endblock %}
{% block content %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>{{ category.name }}</h2>
<a href="{{ url_for('categories') }}" class="btn">← К списку</a>
</div>
<div style="background: white; border: 2px solid #3CB371; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3>Информация о категории</h3>
<p><strong>Описание:</strong> {{ category.description or '—' }}</p>
</div>
<div style="background: white; border: 2px solid #3CB371; border-radius: 8px; padding: 20px;">
<h3>Товары в категории</h3>
{% if products %}
<table>
<thead>
<tr>
<th>Артикул</th>
<th>Наименование</th>
<th>Цена</th>
<th>Алкоголь</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>{{ product.article }}</td>
<td>{{ product.name }}</td>
<td>{{ product.base_price }} ₽</td>
<td>{% if product.is_alcohol %}✅{% else %}❌{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>В категории нет товаров</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}{{ 'Редактирование' if category else 'Добавление' }} категории{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ '✏️ Редактирование категории' if category else '➕ Добавление новой категории' }}</h2>
<form method="POST" onsubmit="return validateForm()">
<div class="form-group">
<label for="name">Название категории *</label>
<input type="text" id="name" name="name" value="{{ category.name if category else '' }}" required
minlength="2" maxlength="255" pattern="[А-Яа-яЁёA-Za-z0-9\s\-]+"
title="Только буквы, цифры, пробелы и дефисы">
</div>
<div class="form-group">
<label for="description">Описание</label>
<textarea id="description" name="description" rows="3" maxlength="1000">{{ category.description if category else '' }}</textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{ '💾 Сохранить' if category else '✅ Добавить' }}</button>
<a href="{{ url_for('categories') }}" class="btn">↩️ Отмена</a>
</div>
</form>
</div>
<script>
function validateForm() {
const name = document.getElementById('name').value.trim();
if (name.length < 2) {
alert('Название категории должно содержать минимум 2 символа');
return false;
}
return true;
}
</script>
{% endblock %}

52
templates/index.html Normal file
View file

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}Заявки{% endblock %}
{% block content %}
<div class="page-header">
<h2>📋 Заявки партнеров</h2>
<a href="{{ url_for('new_order') }}" class="btn btn-primary">➕ Новая заявка</a>
</div>
{% if orders %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Номер</th>
<th>Дата</th>
<th>Партнер</th>
<th>Сумма</th>
<th>Позиций</th>
<th>Действия</th>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td><strong>{{ order.order_number|default('?') }}</strong>\\
<td>{{ order.date_created.strftime('%d.%m.%Y %H:%M') if order.date_created else '—' }}</td>
<td>
{% if order.partner %}
{{ order.partner.name|default('Без имени') }}
{% else %}
<span style="color: red;">Партнёр удалён</span>
{% endif %}
</td>
<td><strong>{{ order.calculate_total()|default(0) }} ₽</strong></td>
<td>{{ order.items|length|default(0) }}</td>
<td class="actions">
<a href="{{ url_for('order_detail', id=order.id|default(0)) }}" class="btn btn-small">👁️</a>
<a href="{{ url_for('delete_order', id=order.id|default(0)) }}" class="btn btn-small btn-delete" onclick="return confirm('Удалить заявку?')">🗑️</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<p>📭 Нет заявок</p>
<a href="{{ url_for('new_order') }}" class="btn btn-primary">➕ Создать первую заявку</a>
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Заявка {{ order.order_number|default('?') }}{% endblock %}
{% block content %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>Заявка {{ order.order_number|default('?') }}</h2>
<a href="{{ url_for('index') }}" class="btn">← К списку</a>
</div>
<div style="background: white; border: 2px solid #3CB371; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3>Информация о заявке</h3>
<table style="width: 100%;">
<tr>
<th style="width: 200px; padding: 10px;">Дата создания:</th>
<td>{{ order.date_created.strftime('%d.%m.%Y %H:%M') if order.date_created else '—' }}</td>
</tr>
<tr>
<th style="padding: 10px;">Партнер:</th>
<td>
{% if order.partner %}
<a href="{{ url_for('partner_detail', id=order.partner.id|default(0)) }}">
{{ order.partner.name|default('Партнёр удалён') }}
</a>
{% else %}
<span style="color: red;">Партнёр удалён (ID: {{ order.partner_id|default('?') }})</span>
{% endif %}
</td>
</tr>
</table>
</div>
<div style="background: white; border: 2px solid #3CB371; border-radius: 8px; padding: 20px;">
<h3>Состав заявки</h3>
<table class="table">
<thead>
<tr>
<th>Артикул</th>
<th>Наименование</th>
<th>Количество</th>
<th>Цена</th>
<th>Акциз</th>
<th>Сбор</th>
<th>Сумма</th>
</tr>
</thead>
<tbody>
{% for item in order.items %}
<tr>
<td>{{ item.product.article|default('—') }}</td>
<td>{{ item.product.name|default('—') }}</td>
<td>{{ item.quantity|default(0) }} {{ item.product.unit|default('шт') }}</td>
<td>{{ item.price_per_unit|default(0) }} ₽</td>
<td>{{ item.product.excise_tax|default(0) }} ₽</td>
<td>{{ item.product.license_fee|default(0) }} ₽</td>
<td><strong>{{ item.calculate_subtotal()|default(0) }} ₽</strong></td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="6" style="text-align: right;"><strong>ИТОГО:</strong></td>
<td><strong>{{ order.calculate_total()|default(0) }} ₽</strong></td>
</tr>
</tfoot>
</table>
</div>
{% endblock %}

353
templates/order_form.html Normal file
View file

@ -0,0 +1,353 @@
{% extends "base.html" %}
{% block title %}{{ 'Редактирование' if order else 'Новая' }} заявка{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ '✏️ Редактирование заявки' if order else '➕ Новая заявка' }}</h2>
<form method="POST" id="orderForm">
<div class="form-group">
<label for="partner_id">Партнер *</label>
<select id="partner_id" name="partner_id" required>
<option value="">— Выберите партнера —</option>
{% for partner in partners %}
<option value="{{ partner.id|default(0) }}"
data-discount="{{ partner.discount|default(0) }}"
{% if order and order.partner_id == partner.id %}selected{% endif %}>
{{ partner.name|default('Без имени') }} (скидка: {{ partner.discount|default(0) }}%)
</option>
{% endfor %}
</select>
</div>
<h3>Позиции заявки</h3>
<div id="items-container">
{% if order %}
{% for item in order.items %}
<div class="order-item" data-item-id="{{ item.id }}">
<select name="product_id[]" required class="product-select" disabled>
<option value="">Выберите товар</option>
{% for product in products %}
<option value="{{ product.id }}"
data-price="{{ product.base_price }}"
data-excise="{{ product.excise_tax }}"
data-license="{{ product.license_fee }}"
data-name="{{ product.name }}"
{% if item.product_id == product.id %}selected{% endif %}>
{{ product.article }} - {{ product.name }}
</option>
{% endfor %}
</select>
<input type="number" name="quantity[]" step="0.001" min="0.001"
value="{{ item.quantity }}" placeholder="Кол-во"
required disabled class="quantity-input">
<div class="order-item-price">
<div class="price-container">
<label>Цена за ед.:</label>
<input type="number" name="price[]" step="0.01" min="0"
value="{{ item.price_per_unit }}" placeholder="Цена"
required disabled class="price-input"
readonly style="background-color: #f0f0f0;">
</div>
<div class="total-container">
<label>Итого:</label>
<span class="item-subtotal">{{ item.calculate_subtotal() }} ₽</span>
</div>
<div class="price-breakdown">
Базовая: {{ item.product.base_price }} ₽ |
Акциз: {{ item.product.excise_tax }} ₽ |
Сбор: {{ item.product.license_fee }} ₽
{% if order.partner and order.partner.discount > 0 %}
| Скидка: {{ order.partner.discount }}%
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="order-item">
<select name="product_id[]" required class="product-select">
<option value="">— Выберите товар —</option>
{% for product in products %}
<option value="{{ product.id }}"
data-price="{{ product.base_price }}"
data-excise="{{ product.excise_tax }}"
data-license="{{ product.license_fee }}"
data-name="{{ product.name }}">
{{ product.article }} - {{ product.name }} ({{ product.base_price }} ₽)
</option>
{% endfor %}
</select>
<input type="number" name="quantity[]" step="0.001" min="0.001"
placeholder="Количество" required class="quantity-input">
<button type="button" class="btn btn-delete" onclick="this.parentElement.remove(); updateGrandTotal();">❌</button>
<div class="order-item-price">
<div class="price-container">
<label>Цена за ед.:</label>
<input type="number" name="price[]" step="0.01" min="0"
placeholder="Цена" required class="price-input"
readonly style="background-color: #f0f0f0;">
</div>
<div class="total-container">
<label>Итого:</label>
<span class="item-subtotal">0.00 ₽</span>
</div>
<div class="price-breakdown"></div>
</div>
</div>
{% endif %}
</div>
{% if not order %}
<button type="button" class="btn" onclick="addItem()">➕ Добавить позицию</button>
{% endif %}
<div class="order-summary" style="margin: 20px 0; padding: 20px; background: #F5FFFA; border: 2px solid #3CB371; border-radius: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0; color: #3CB371;">ИТОГО:</h3>
<h2 style="margin: 0; color: #3CB371;" id="grand-total">0.00 ₽</h2>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
{% if order %}💾 Сохранить{% else %}✅ Создать заявку{% endif %}
</button>
<a href="{{ url_for('index') }}" class="btn">↩️ Отмена</a>
</div>
</form>
</div>
<script>
function updateProductInfo(selectElement) {
const itemElement = selectElement.closest('.order-item');
const priceInput = itemElement.querySelector('.price-input');
const priceBreakdown = itemElement.querySelector('.price-breakdown');
const selectedOption = selectElement.options[selectElement.selectedIndex];
if (selectedOption && selectedOption.value) {
const basePrice = parseFloat(selectedOption.dataset.price) || 0;
const excise = parseFloat(selectedOption.dataset.excise) || 0;
const license = parseFloat(selectedOption.dataset.license) || 0;
priceInput.value = basePrice.toFixed(2);
priceInput.readOnly = false;
priceInput.style.backgroundColor = '#ffffff';
priceBreakdown.innerHTML = `Базовая цена: ${basePrice.toFixed(2)} ₽ | Акциз: ${excise.toFixed(2)} ₽ | Сбор: ${license.toFixed(2)} ₽`;
} else {
priceInput.value = '';
priceInput.readOnly = true;
priceInput.style.backgroundColor = '#f0f0f0';
priceBreakdown.innerHTML = '';
}
calculateItemTotal(itemElement);
updateGrandTotal();
}
function calculateItemTotal(itemElement) {
const select = itemElement.querySelector('.product-select');
const quantityInput = itemElement.querySelector('.quantity-input');
const priceInput = itemElement.querySelector('.price-input');
const subtotalSpan = itemElement.querySelector('.item-subtotal');
const quantity = parseFloat(quantityInput.value) || 0;
const price = parseFloat(priceInput.value) || 0;
const selectedOption = select.options[select.selectedIndex];
const excise = selectedOption && selectedOption.value ? parseFloat(selectedOption.dataset.excise) || 0 : 0;
const license = selectedOption && selectedOption.value ? parseFloat(selectedOption.dataset.license) || 0 : 0;
const subtotal = (price + excise + license) * quantity;
subtotalSpan.textContent = subtotal.toFixed(2) + ' ₽';
return subtotal;
}
function updateGrandTotal() {
const items = document.querySelectorAll('.order-item');
let grandTotal = 0;
items.forEach(item => {
grandTotal += calculateItemTotal(item) || 0;
});
document.getElementById('grand-total').textContent = grandTotal.toFixed(2) + ' ₽';
}
function addItem() {
const container = document.getElementById('items-container');
const newItem = document.createElement('div');
newItem.className = 'order-item';
const firstSelect = document.querySelector('.product-select');
const optionsHTML = firstSelect ? firstSelect.innerHTML : '';
newItem.innerHTML = `
<select name="product_id[]" required class="product-select">
${optionsHTML}
</select>
<input type="number" name="quantity[]" step="0.001" min="0.001" placeholder="Количество" required class="quantity-input">
<button type="button" class="btn btn-delete" onclick="this.parentElement.remove(); updateGrandTotal();">❌</button>
<div class="order-item-price">
<div class="price-container">
<label>Цена за ед.:</label>
<input type="number" name="price[]" step="0.01" min="0" placeholder="Цена" required class="price-input" readonly>
</div>
<div class="total-container">
<label>Итого:</label>
<span class="item-subtotal">0.00 ₽</span>
</div>
<div class="price-breakdown"></div>
</div>
`;
container.appendChild(newItem);
}
document.addEventListener('change', function(e) {
if (e.target.classList.contains('product-select')) {
updateProductInfo(e.target);
}
});
document.addEventListener('input', function(e) {
if (e.target.classList.contains('quantity-input')) {
const itemElement = e.target.closest('.order-item');
calculateItemTotal(itemElement);
updateGrandTotal();
}
});
document.addEventListener('input', function(e) {
if (e.target.classList.contains('price-input') && !e.target.readOnly) {
const itemElement = e.target.closest('.order-item');
calculateItemTotal(itemElement);
updateGrandTotal();
e.target.style.borderColor = '#FFA500';
setTimeout(() => {
e.target.style.borderColor = '#3CB371';
}, 1000);
}
});
document.getElementById('partner_id').addEventListener('change', function() {
updateGrandTotal();
});
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.product-select').forEach(select => {
if (select.value) {
updateProductInfo(select);
}
});
updateGrandTotal();
});
</script>
<style>
.order-item {
display: grid;
grid-template-columns: 2fr 1fr 0.3fr;
gap: 10px;
margin-bottom: 15px;
padding: 15px;
background: white;
border: 1px solid #3CB371;
border-radius: 8px;
align-items: center;
}
.order-item-price {
grid-column: 1 / -1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 10px;
padding: 10px;
background: #F5FFFA;
border-radius: 5px;
border-left: 3px solid #3CB371;
}
.price-container {
display: flex;
align-items: center;
gap: 10px;
}
.price-container label {
font-weight: bold;
color: #3CB371;
min-width: 80px;
}
.price-container input {
flex: 1;
padding: 8px;
border: 2px solid #3CB371;
border-radius: 5px;
font-weight: bold;
background-color: white;
}
.total-container {
display: flex;
align-items: center;
gap: 10px;
justify-content: flex-end;
}
.total-container label {
font-weight: bold;
color: #3CB371;
min-width: 80px;
}
.total-container .item-subtotal {
font-size: 1.2em;
font-weight: bold;
color: #3CB371;
background: white;
padding: 8px 15px;
border-radius: 5px;
border: 2px solid #3CB371;
min-width: 120px;
text-align: right;
}
.price-breakdown {
grid-column: 1 / -1;
color: #666;
font-size: 0.85em;
margin-top: 5px;
padding-top: 5px;
border-top: 1px dashed #3CB371;
}
@media (max-width: 768px) {
.order-item {
grid-template-columns: 1fr;
}
.order-item-price {
grid-template-columns: 1fr;
}
.price-container, .total-container {
flex-direction: column;
align-items: stretch;
}
}
</style>
{% endblock %}

View file

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% block title %}{{ partner.name }}{% endblock %}
{% block content %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>{{ partner.name }}</h2>
<a href="{{ url_for('partners') }}" class="btn">← К списку</a>
</div>
<div style="background: white; border: 2px solid #3CB371; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3>Информация о партнере</h3>
<table style="width: 100%;">
<tr>
<th style="width: 200px; padding: 10px;">Полное наименование:</th>
<td>{{ partner.name }}</td>
</tr>
<tr>
<th style="padding: 10px;">Сокращенное:</th>
<td>{{ partner.short_name or '—' }}</td>
</tr>
<tr>
<th style="padding: 10px;">Тип:</th>
<td>{{ partner.partner_type or '—' }}</td>
</tr>
<tr>
<th style="padding: 10px;">Руководитель:</th>
<td>{{ partner.director or '—' }}</td>
</tr>
<tr>
<th style="padding: 10px;">Телефон:</th>
<td>{{ partner.phone or '—' }}</td>
</tr>
<tr>
<th style="padding: 10px;">Фактический адрес:</th>
<td>{{ partner.actual_address or '—' }}</td>
</tr>
<tr>
<th style="padding: 10px;">Юридический адрес:</th>
<td>{{ partner.legal_address or '—' }}</td>
</tr>
</table>
</div>
<div style="background: white; border: 2px solid #3CB371; border-radius: 8px; padding: 20px;">
<h3>Заявки партнера</h3>
{% if orders %}
<table>
<thead>
<tr>
<th>Номер</th>
<th>Дата</th>
<th>Статус</th>
<th>Сумма</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>{{ order.order_number }}</td>
<td>{{ order.date_created.strftime('%d.%m.%Y') }}</td>
<td>{{ order.status }}</td>
<td>{{ order.calculate_total() }} ₽</td>
<td>
<a href="{{ url_for('order_detail', id=order.id) }}" class="btn btn-small">👁️</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>У партнера нет заявок</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,89 @@
{% extends "base.html" %}
{% block title %}{{ 'Редактирование' if partner else 'Добавление' }} партнера{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ '✏️ Редактирование партнера' if partner else '➕ Новый партнер' }}</h2>
<form method="POST" onsubmit="return validateForm()">
<div class="form-group">
<label for="name">Полное наименование *</label>
<input type="text" id="name" name="name" value="{{ partner.name if partner else '' }}" required
minlength="2" maxlength="255" pattern="[А-Яа-яЁёA-Za-z0-9\s\-\.\,\"\']+"
title="Только буквы, цифры, пробелы и знаки препинания">
</div>
<div class="form-group">
<label for="short_name">Сокращенное наименование</label>
<input type="text" id="short_name" name="short_name" value="{{ partner.short_name if partner else '' }}"
maxlength="100">
</div>
<div class="form-group">
<label for="partner_type">Тип партнера</label>
<select id="partner_type" name="partner_type">
<option value="">— Выберите тип —</option>
<option value="ООО" {% if partner and partner.partner_type == 'ООО' %}selected{% endif %}>ООО</option>
<option value="ИП" {% if partner and partner.partner_type == 'ИП' %}selected{% endif %}>ИП</option>
<option value="АО" {% if partner and partner.partner_type == 'АО' %}selected{% endif %}>АО</option>
<option value="ЗАО" {% if partner and partner.partner_type == 'ЗАО' %}selected{% endif %}>ЗАО</option>
<option value="ОАО" {% if partner and partner.partner_type == 'ОАО' %}selected{% endif %}>ОАО</option>
</select>
</div>
<div class="form-group">
<label for="director">Руководитель</label>
<input type="text" id="director" name="director" value="{{ partner.director if partner else '' }}"
pattern="[А-Яа-яЁёA-Za-z\s\-\.]+" title="Только буквы, пробелы, дефисы и точки">
</div>
<div class="form-group">
<label for="phone">Телефон</label>
<input type="tel" id="phone" name="phone" value="{{ partner.phone if partner else '' }}"
pattern="[\+d][\d\(\)\-\s]{9,20}" title="Введите номер телефона в формате +7 (123) 456-78-90"
placeholder="+7 (123) 456 78 90">
<small>Пример: +7(123)-456-78-90</small>
</div>
<div class="form-group">
<label for="discount">Скидка (%)</label>
<input type="number" id="discount" name="discount" step="0.1" min="0" max="100"
value="{{ partner.discount if partner else '0' }}">
</div>
<div class="form-group">
<label for="actual_address">Фактический адрес</label>
<textarea id="actual_address" name="actual_address" rows="2" maxlength="500">{{ partner.actual_address if partner else '' }}</textarea>
</div>
<div class="form-group">
<label for="legal_address">Юридический адрес</label>
<textarea id="legal_address" name="legal_address" rows="2" maxlength="500">{{ partner.legal_address if partner else '' }}</textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{ '💾 Сохранить' if partner else '✅ Добавить' }}</button>
<a href="{{ url_for('partners') }}" class="btn">↩️ Отмена</a>
</div>
</form>
</div>
<script>
function validateForm() {
const name = document.getElementById('name').value.trim();
if (name.length < 2) {
alert('Название партнера должно содержать минимум 2 символа');
return false;
}
const discount = parseFloat(document.getElementById('discount').value);
if (isNaN(discount) || discount < 0 || discount > 100) {
alert('Скидка должна быть от 0 до 100%');
return false;
}
return true;
}
</script>
{% endblock %}

46
templates/partners.html Normal file
View file

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}Партнеры{% endblock %}
{% block content %}
<div class="page-header">
<h2>🤝 Партнеры</h2>
<a href="{{ url_for('new_partner') }}" class="btn btn-primary">➕ Новый партнер</a>
</div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Наименование</th>
<th>Тип</th>
<th>Руководитель</th>
<th>Телефон</th>
<th>Сумма заказов</th>
<th>Скидка</th>
<th>Заявок</th>
<th>Действия</th>
</thead>
<tbody>
{% for partner in partners %}
{% if partner and partner.id %}
<tr>
<td>{{ partner.id|default('?') }}</td>
<td><strong>{{ partner.name|default('Без имени') }}</strong></td>
<td>{{ partner.partner_type|default('—') }}</td>
<td>{{ partner.director|default('—') }}</td>
<td>{{ partner.phone|default('—') }}</td>
<td><strong>{{ "%.2f"|format(partner.total_orders_sum|default(0)) }} ₽</strong></td>
<td><strong style="color: #3CB371;">{{ partner.discount|default(0) }}%</strong></td>
<td>{{ partner.orders|length|default(0) }}</td>
<td class="actions">
<a href="{{ url_for('partner_detail', id=partner.id|default(0)) }}" class="btn btn-small">👁️</a>
<a href="{{ url_for('edit_partner', id=partner.id|default(0)) }}" class="btn btn-small">✏️</a>
<a href="{{ url_for('delete_partner', id=partner.id|default(0)) }}" class="btn btn-small btn-delete" onclick="return confirm('Удалить партнера?')">🗑️</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -0,0 +1,98 @@
{% extends "base.html" %}
{% block title %}{{ 'Редактирование' if product else 'Добавление' }} товара{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ '✏️ Редактирование товара' if product else '➕ Добавление нового товара' }}</h2>
<form method="POST">
<div class="form-group">
<label for="article">Артикул *</label>
<input type="text" id="article" name="article" value="{{ product.article if product else '' }}" required>
</div>
<div class="form-group">
<label for="name">Наименование *</label>
<input type="text" id="name" name="name" value="{{ product.name if product else '' }}" required>
</div>
<div class="form-group">
<label for="commercial_name">Коммерческое наименование</label>
<input type="text" id="commercial_name" name="commercial_name" value="{{ product.commercial_name if product else '' }}">
</div>
<div class="form-row" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label for="unit">Единица измерения</label>
<input type="text" id="unit" name="unit" value="{{ product.unit if product else 'шт' }}">
</div>
<div class="form-group">
<label for="category_id">Категория *</label>
<select id="category_id" name="category_id" required>
<option value="">— Выберите категорию —</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if product and product.category_id == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-row" style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px;">
<div class="form-group">
<label for="base_price">Базовая цена *</label>
<input type="number" id="base_price" name="base_price" step="0.01" min="0" value="{{ product.base_price if product else '' }}" required>
</div>
<div class="form-group">
<label for="excise_tax">Акциз (₽)</label>
<input type="number" id="excise_tax" name="excise_tax" step="0.01" min="0" value="{{ product.excise_tax if product else '0' }}">
</div>
<div class="form-group">
<label for="license_fee">Лицензионный сбор (₽)</label>
<input type="number" id="license_fee" name="license_fee" step="0.01" min="0" value="{{ product.license_fee if product else '0' }}">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_alcohol" {% if product and product.is_alcohol %}checked{% endif %}>
Алкогольная продукция
</label>
</div>
<div class="form-row" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;" id="alcohol-fields" {% if not product or not product.is_alcohol %}style="display: none;"{% endif %}>
<div class="form-group">
<label for="alcohol_content">Крепость (%)</label>
<input type="number" id="alcohol_content" name="alcohol_content" step="0.1" min="0" max="100" value="{{ product.alcohol_content if product else '' }}">
</div>
<div class="form-group">
<label for="volume">Объём (л)</label>
<input type="number" id="volume" name="volume" step="0.001" min="0" value="{{ product.volume if product else '' }}">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{ '💾 Сохранить' if product else '✅ Добавить' }}</button>
<a href="{{ url_for('products') }}" class="btn">↩️ Отмена</a>
</div>
</form>
</div>
<script>
// Показывать/скрывать поля для алкоголя
document.querySelector('input[name="is_alcohol"]').addEventListener('change', function(e) {
const alcoholFields = document.getElementById('alcohol-fields');
if (e.target.checked) {
alcoholFields.style.display = 'grid';
} else {
alcoholFields.style.display = 'none';
}
});
</script>
{% endblock %}

58
templates/products.html Normal file
View file

@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block title %}Товары{% endblock %}
{% block content %}
<div class="page-header">
<h2>📦 Каталог товаров</h2>
<a href="{{ url_for('new_product') }}" class="btn btn-primary">➕ Новый товар</a>
</div>
<div class="filters" style="margin: 20px 0;">
<form method="GET" style="display: flex; gap: 10px;">
<input type="text" name="search" placeholder="Поиск по названию..." value="{{ request.args.get('search', '') }}" style="flex: 1;">
<select name="category">
<option value="">Все категории</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if request.args.get('category')|int == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
<button type="submit" class="btn">🔍 Поиск</button>
<a href="{{ url_for('products') }}" class="btn">Сбросить</a>
</form>
</div>
<table>
<thead>
<tr>
<th>Артикул</th>
<th>Наименование</th>
<th>Категория</th>
<th>Цена</th>
<th>Акциз</th>
<th>Сбор</th>
<th>Алкоголь</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>{{ product.article }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category.name }}</td>
<td>{{ product.base_price }} ₽</td>
<td>{{ product.excise_tax }} ₽</td>
<td>{{ product.license_fee }} ₽</td>
<td>{% if product.is_alcohol %}✅{% else %}❌{% endif %}</td>
<td class="actions">
<a href="{{ url_for('edit_product', id=product.id) }}" class="btn btn-small" title="Редактировать">✏️</a>
<a href="{{ url_for('delete_product', id=product.id) }}" class="btn btn-small btn-delete" onclick="return confirm('Удалить товар {{ product.name }}?')" title="Удалить">🗑️</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}