Add files via upload
This commit is contained in:
commit
6b1aeb9594
4
README.md
Normal file
4
README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
умма заказов Скидка
|
||||||
|
от 10 000 ₽ 5%
|
||||||
|
от 20 000 ₽ 10%
|
||||||
|
от 50 000 ₽ 20%
|
||||||
432
app.py
Normal file
432
app.py
Normal 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
BIN
data/categories.xlsx
Normal file
Binary file not shown.
BIN
data/partners.xlsx
Normal file
BIN
data/partners.xlsx
Normal file
Binary file not shown.
BIN
data/products.xlsx
Normal file
BIN
data/products.xlsx
Normal file
Binary file not shown.
123
init_db.py
Normal file
123
init_db.py
Normal 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
BIN
instance/warehouse.db
Normal file
Binary file not shown.
94
reset_db.py
Normal file
94
reset_db.py
Normal 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
262
static/css/style.css
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
static/images/5294256562463512548.jpg
Normal file
BIN
static/images/5294256562463512548.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
47
templates/base.html
Normal file
47
templates/base.html
Normal 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
37
templates/categories.html
Normal 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 %}
|
||||||
43
templates/category_detail.html
Normal file
43
templates/category_detail.html
Normal 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 %}
|
||||||
39
templates/category_form.html
Normal file
39
templates/category_form.html
Normal 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
52
templates/index.html
Normal 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 %}
|
||||||
68
templates/order_detail.html
Normal file
68
templates/order_detail.html
Normal 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
353
templates/order_form.html
Normal 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 %}
|
||||||
76
templates/partner_detail.html
Normal file
76
templates/partner_detail.html
Normal 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 %}
|
||||||
89
templates/partner_form.html
Normal file
89
templates/partner_form.html
Normal 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
46
templates/partners.html
Normal 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 %}
|
||||||
98
templates/product_form.html
Normal file
98
templates/product_form.html
Normal 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
58
templates/products.html
Normal 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 %}
|
||||||
Loading…
Reference in a new issue