366 lines
14 KiB
Python
366 lines
14 KiB
Python
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||
from . import db, csrf
|
||
from .models import Partners, Inventory, Categories, IssueOrders, IssueOrderItems, ReturnOrders, ReturnOrderItems
|
||
from .utils import calculate_item_price, get_current_season_name
|
||
from datetime import datetime
|
||
|
||
bp = Blueprint('main', __name__)
|
||
|
||
|
||
# Главная
|
||
@bp.route('/')
|
||
def index():
|
||
active_issues = IssueOrders.query.filter(~IssueOrders.return_order.has()).count()
|
||
partners_count = Partners.query.count()
|
||
expected_total = 0
|
||
for issue in IssueOrders.query.filter(~IssueOrders.return_order.has()).all():
|
||
for item in issue.items:
|
||
expected_total += float(item.inventory.base_price) * item.quantity
|
||
recent_issues = IssueOrders.query.order_by(IssueOrders.issue_date.desc()).limit(5).all()
|
||
recent_returns = ReturnOrders.query.order_by(ReturnOrders.return_date.desc()).limit(5).all()
|
||
return render_template('index.html',
|
||
active_issues=active_issues,
|
||
partners_count=partners_count,
|
||
expected_total=round(expected_total, 2),
|
||
recent_issues=recent_issues,
|
||
recent_returns=recent_returns)
|
||
|
||
|
||
# Партнёры
|
||
@bp.route('/partners')
|
||
def partners():
|
||
search = request.args.get('search', '')
|
||
query = Partners.query
|
||
if search:
|
||
query = query.filter(
|
||
(Partners.name.contains(search)) | (Partners.phone.contains(search))
|
||
)
|
||
all_partners = query.all()
|
||
return render_template('partners.html', partners=all_partners)
|
||
|
||
|
||
@bp.route('/partners/new', methods=['GET', 'POST'])
|
||
def partner_new():
|
||
if request.method == 'POST':
|
||
name = request.form['name']
|
||
phone = request.form['phone'].strip()
|
||
email = request.form['email']
|
||
address = request.form['address']
|
||
|
||
# Валидация телефона
|
||
if phone:
|
||
digits = ''.join(filter(str.isdigit, phone))
|
||
if len(digits) < 10 or len(digits) > 15:
|
||
flash('Телефон должен содержать от 10 до 15 цифр (например, +7(999)123-45-67)', 'danger')
|
||
# Создаём временный объект для возврата в форму
|
||
temp_partner = Partners(name=name, phone=phone, email=email, address=address)
|
||
return render_template('partner_form.html', partner=temp_partner)
|
||
|
||
p = Partners(name=name, phone=phone, email=email, address=address)
|
||
db.session.add(p)
|
||
db.session.commit()
|
||
flash('Партнёр добавлен', 'success')
|
||
return redirect(url_for('main.partners'))
|
||
|
||
return render_template('partner_form.html')
|
||
|
||
|
||
@bp.route('/partners/<int:id>')
|
||
def partner_detail(id):
|
||
partner = Partners.query.get_or_404(id)
|
||
return render_template('partner_detail.html', partner=partner)
|
||
|
||
|
||
@bp.route('/partners/<int:id>/edit', methods=['GET', 'POST'])
|
||
def partner_edit(id):
|
||
partner = Partners.query.get_or_404(id)
|
||
if request.method == 'POST':
|
||
name = request.form['name']
|
||
phone = request.form['phone'].strip()
|
||
email = request.form['email']
|
||
address = request.form['address']
|
||
|
||
# Сначала обновляем поля (ещё не коммитим)
|
||
partner.name = name
|
||
partner.phone = phone
|
||
partner.email = email
|
||
partner.address = address
|
||
|
||
# Валидация телефона
|
||
if phone:
|
||
digits = ''.join(filter(str.isdigit, phone))
|
||
if len(digits) < 10 or len(digits) > 15:
|
||
flash('Телефон должен содержать от 10 до 15 цифр (например, +7(999)123-45-67)', 'danger')
|
||
return render_template('partner_form.html', partner=partner)
|
||
|
||
db.session.commit()
|
||
flash('Данные обновлены', 'success')
|
||
return redirect(url_for('main.partner_detail', id=id))
|
||
|
||
return render_template('partner_form.html', partner=partner)
|
||
|
||
|
||
# Инвентарь
|
||
@bp.route('/inventory')
|
||
def inventory():
|
||
category_id = request.args.get('category', type=int)
|
||
season = request.args.get('season', '')
|
||
query = Inventory.query
|
||
if category_id:
|
||
query = query.filter_by(category_id=category_id)
|
||
if season in ['winter', 'summer']:
|
||
query = query.filter_by(season_type=season)
|
||
items = query.all()
|
||
categories = Categories.query.all()
|
||
return render_template('inventory.html',
|
||
items=items,
|
||
categories=categories,
|
||
selected_category=category_id,
|
||
selected_season=season)
|
||
|
||
|
||
@bp.route('/inventory/new', methods=['GET', 'POST'])
|
||
def inventory_new():
|
||
if request.method == 'POST':
|
||
name = request.form['name']
|
||
category_id = request.form['category_id']
|
||
base_price = request.form['base_price']
|
||
season_type = request.form['season_type']
|
||
inv = Inventory(name=name, category_id=category_id, base_price=base_price, season_type=season_type)
|
||
db.session.add(inv)
|
||
db.session.commit()
|
||
flash('Инвентарь добавлен', 'success')
|
||
return redirect(url_for('main.inventory'))
|
||
categories = Categories.query.all()
|
||
return render_template('inventory_form.html', categories=categories)
|
||
|
||
|
||
@bp.route('/inventory/<int:id>/edit', methods=['GET', 'POST'])
|
||
def inventory_edit(id):
|
||
inv = Inventory.query.get_or_404(id)
|
||
if request.method == 'POST':
|
||
inv.name = request.form['name']
|
||
inv.category_id = request.form['category_id']
|
||
inv.base_price = request.form['base_price']
|
||
inv.season_type = request.form['season_type']
|
||
db.session.commit()
|
||
flash('Изменения сохранены', 'success')
|
||
return redirect(url_for('main.inventory'))
|
||
categories = Categories.query.all()
|
||
return render_template('inventory_form.html', item=inv, categories=categories)
|
||
|
||
|
||
@bp.route('/inventory/<int:id>/delete', methods=['POST'])
|
||
def inventory_delete(id):
|
||
inv = Inventory.query.get_or_404(id)
|
||
if IssueOrderItems.query.filter_by(inventory_id=id).first() or ReturnOrderItems.query.filter_by(
|
||
inventory_id=id).first():
|
||
flash('Нельзя удалить инвентарь, который был выдан', 'danger')
|
||
else:
|
||
db.session.delete(inv)
|
||
db.session.commit()
|
||
flash('Инвентарь удалён', 'success')
|
||
return redirect(url_for('main.inventory'))
|
||
|
||
|
||
# Категории
|
||
@bp.route('/categories')
|
||
def categories():
|
||
cats = Categories.query.all()
|
||
return render_template('categories.html', categories=cats)
|
||
|
||
|
||
@bp.route('/categories/new', methods=['GET', 'POST'])
|
||
def category_new():
|
||
if request.method == 'POST':
|
||
name = request.form['name']
|
||
description = request.form['description']
|
||
cat = Categories(name=name, description=description)
|
||
db.session.add(cat)
|
||
db.session.commit()
|
||
flash('Категория добавлена', 'success')
|
||
return redirect(url_for('main.categories'))
|
||
return render_template('category_form.html')
|
||
|
||
|
||
@bp.route('/categories/<int:id>/edit', methods=['GET', 'POST'])
|
||
def category_edit(id):
|
||
cat = Categories.query.get_or_404(id)
|
||
if request.method == 'POST':
|
||
cat.name = request.form['name']
|
||
cat.description = request.form['description']
|
||
db.session.commit()
|
||
flash('Категория обновлена', 'success')
|
||
return redirect(url_for('main.categories'))
|
||
return render_template('category_form.html', category=cat)
|
||
|
||
|
||
@bp.route('/categories/<int:id>/delete', methods=['POST'])
|
||
def category_delete(id):
|
||
cat = Categories.query.get_or_404(id)
|
||
if Inventory.query.filter_by(category_id=id).first():
|
||
flash('Нельзя удалить категорию, в которой есть инвентарь', 'danger')
|
||
else:
|
||
db.session.delete(cat)
|
||
db.session.commit()
|
||
flash('Категория удалена', 'success')
|
||
return redirect(url_for('main.categories'))
|
||
|
||
|
||
# Выдачи
|
||
@bp.route('/issues')
|
||
def issues():
|
||
issues = IssueOrders.query.all()
|
||
return render_template('issues.html', issues=issues)
|
||
|
||
|
||
@bp.route('/issues/new', methods=['GET', 'POST'])
|
||
def issue_new():
|
||
if request.method == 'POST':
|
||
partner_id = request.form['partner_id']
|
||
notes = request.form.get('notes', '')
|
||
issue = IssueOrders(partner_id=partner_id, notes=notes)
|
||
db.session.add(issue)
|
||
db.session.commit()
|
||
inventory_ids = request.form.getlist('inventory_id[]')
|
||
quantities = request.form.getlist('quantity[]')
|
||
for inv_id, qty in zip(inventory_ids, quantities):
|
||
if inv_id and qty:
|
||
item = IssueOrderItems(issue_order_id=issue.id, inventory_id=int(inv_id), quantity=int(qty))
|
||
db.session.add(item)
|
||
db.session.commit()
|
||
flash('Выдача оформлена', 'success')
|
||
return redirect(url_for('main.issues'))
|
||
partners = Partners.query.all()
|
||
inventory = Inventory.query.all()
|
||
return render_template('issue_form.html', partners=partners, inventory=inventory)
|
||
|
||
|
||
@bp.route('/issues/<int:id>')
|
||
def issue_detail(id):
|
||
issue = IssueOrders.query.get_or_404(id)
|
||
return render_template('issue_detail.html', issue=issue)
|
||
|
||
|
||
@bp.route('/issues/<int:id>/edit', methods=['GET', 'POST'])
|
||
def issue_edit(id):
|
||
issue = IssueOrders.query.get_or_404(id)
|
||
if issue.return_order:
|
||
flash('Нельзя редактировать закрытую выдачу', 'danger')
|
||
return redirect(url_for('main.issue_detail', id=id))
|
||
if request.method == 'POST':
|
||
issue.notes = request.form.get('notes', '')
|
||
IssueOrderItems.query.filter_by(issue_order_id=id).delete()
|
||
inventory_ids = request.form.getlist('inventory_id[]')
|
||
quantities = request.form.getlist('quantity[]')
|
||
for inv_id, qty in zip(inventory_ids, quantities):
|
||
if inv_id and qty:
|
||
item = IssueOrderItems(issue_order_id=id, inventory_id=int(inv_id), quantity=int(qty))
|
||
db.session.add(item)
|
||
db.session.commit()
|
||
flash('Выдача обновлена', 'success')
|
||
return redirect(url_for('main.issue_detail', id=id))
|
||
partners = Partners.query.all()
|
||
inventory = Inventory.query.all()
|
||
return render_template('issue_form.html', issue=issue, partners=partners, inventory=inventory)
|
||
|
||
|
||
# Возвраты
|
||
@bp.route('/returns')
|
||
def returns():
|
||
returns = ReturnOrders.query.all()
|
||
return render_template('returns.html', returns=returns)
|
||
|
||
|
||
@bp.route('/returns/new', methods=['GET', 'POST'])
|
||
def return_new():
|
||
if request.method == 'POST':
|
||
issue_id = request.form['issue_id']
|
||
|
||
# Проверка на существующий возврат
|
||
existing_return = ReturnOrders.query.filter_by(issue_order_id=issue_id).first()
|
||
if existing_return:
|
||
flash('Возврат по этой выдаче уже оформлен', 'danger')
|
||
return redirect(url_for('main.return_new'))
|
||
|
||
# Получаем список ID позиций
|
||
items_data = request.form.getlist('item_id[]')
|
||
if not items_data:
|
||
flash('Нет позиций для возврата', 'danger')
|
||
return redirect(url_for('main.return_new'))
|
||
|
||
# Собираем степени износа для каждой позиции
|
||
wear_dict = {}
|
||
for item_id in items_data:
|
||
wear = request.form.get(f'wear_{item_id}')
|
||
if not wear:
|
||
flash(f'Не указана степень износа для позиции', 'danger')
|
||
return redirect(url_for('main.return_new'))
|
||
wear_dict[item_id] = wear
|
||
|
||
return_date = datetime.now()
|
||
notes = request.form.get('notes', '')
|
||
|
||
# Создаём возврат (получаем ID, но не коммитим)
|
||
ret = ReturnOrders(issue_order_id=issue_id, return_date=return_date, notes=notes)
|
||
db.session.add(ret)
|
||
db.session.flush() # чтобы получить ID возврата
|
||
|
||
total = 0
|
||
for item_id in items_data:
|
||
issue_item = IssueOrderItems.query.get(int(item_id))
|
||
wear = wear_dict[item_id]
|
||
price = calculate_item_price(
|
||
base_price=issue_item.inventory.base_price,
|
||
quantity=issue_item.quantity,
|
||
wear_level=wear,
|
||
season_type=issue_item.inventory.season_type,
|
||
return_date=return_date
|
||
)
|
||
ret_item = ReturnOrderItems(
|
||
return_order_id=ret.id,
|
||
inventory_id=issue_item.inventory_id,
|
||
quantity=issue_item.quantity,
|
||
wear_level=wear,
|
||
calculated_price=price
|
||
)
|
||
db.session.add(ret_item)
|
||
total += price
|
||
|
||
ret.total_amount = round(total, 2)
|
||
db.session.commit()
|
||
flash('Возврат оформлен', 'success')
|
||
return redirect(url_for('main.returns'))
|
||
|
||
open_issues = IssueOrders.query.filter(~IssueOrders.return_order.has()).all()
|
||
current_season = get_current_season_name()
|
||
return render_template('return_select_issue.html', issues=open_issues, current_season=current_season)
|
||
|
||
|
||
@bp.route('/returns/get_issue_items/<int:issue_id>')
|
||
def get_issue_items(issue_id):
|
||
issue = IssueOrders.query.get_or_404(issue_id)
|
||
items = []
|
||
for item in issue.items:
|
||
items.append({
|
||
'id': item.id,
|
||
'inventory_id': item.inventory_id,
|
||
'inventory_name': item.inventory.name,
|
||
'quantity': item.quantity,
|
||
'base_price': float(item.inventory.base_price),
|
||
'season_type': item.inventory.season_type
|
||
})
|
||
return jsonify(items)
|
||
|
||
|
||
@csrf.exempt
|
||
@bp.route('/returns/calculate_preview', methods=['POST'])
|
||
def calculate_preview():
|
||
data = request.get_json()
|
||
base_price = float(data['base_price'])
|
||
quantity = int(data['quantity'])
|
||
wear_level = data['wear_level']
|
||
season_type = data['season_type']
|
||
return_date = datetime.now()
|
||
price = calculate_item_price(base_price, quantity, wear_level, season_type, return_date)
|
||
return jsonify({'calculated_price': price}) |