Загрузка данных


import os
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_file
from config import Config
from models import db, Employee, ServiceNote, SupplementDetail, CombinationDetail, SubstitutionDetail, AllowanceDetail
from datetime import datetime
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, Alignment, Border, Side
from openpyxl.utils import range_boundaries
from openpyxl.cell.cell import MergedCell
from copy import copy
from io import BytesIO

app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)

# Путь к папке с шаблонами Excel
TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'excel_templates')

# ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================

def safe_write(ws, row, col, value):
    """Безопасная запись в ячейку. Если ячейка объединена, пишет в верхнюю левую ячейку диапазона."""
    cell = ws.cell(row=row, column=col)
    if isinstance(cell, MergedCell):
        for merged_range in ws.merged_cells.ranges:
            if cell.coordinate in merged_range:
                min_col, min_row, _, _ = range_boundaries(merged_range.coord)
                ws.cell(row=min_row, column=min_col).value = value
                return
    else:
        cell.value = value

def copy_cell_style(source_cell, target_cell):
    """Копирует стиль из одной ячейки в другую."""
    if source_cell.has_style:
        target_cell.font = copy(source_cell.font)
        target_cell.border = copy(source_cell.border)
        target_cell.fill = copy(source_cell.fill)
        target_cell.number_format = copy(source_cell.number_format)
        target_cell.protection = copy(source_cell.protection)
        target_cell.alignment = copy(source_cell.alignment)

def format_date(date_obj):
    return date_obj.strftime('%d.%m.%Y') if date_obj else ''

# ==================== ГЛАВНАЯ СТРАНИЦА ====================

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/notes')
def api_notes():
    page = request.args.get('page', 1, type=int)
    per_page = 15
    payment_type = request.args.get('type', '')
    
    query = ServiceNote.query
    if payment_type:
        query = query.filter(ServiceNote.payment_code == payment_type)
    
    notes = query.order_by(ServiceNote.created_at.desc()).paginate(page=page, per_page=per_page)
    
    items = []
    for note in notes.items:
        employee_name = '-'
        amount = '-'
        start_date = ''
        end_date = ''
        
        if note.payment_code in ['1106', '1006']:
            detail = SupplementDetail.query.filter_by(service_note_id=note.id).first()
            if detail and detail.employee:
                employee_name = detail.employee.full_name
                amount = f"{detail.amount} {'%' if detail.is_percent else 'руб'}"
                start_date = format_date(detail.start_date)
                end_date = format_date(detail.end_date)
        elif note.payment_code in ['1003', '1021', '1001']:
            detail = CombinationDetail.query.filter_by(service_note_id=note.id).first()
            if detail and detail.employee:
                employee_name = detail.employee.full_name
                amount = f"{detail.extra_pay} руб" if detail.extra_pay else '-'
                start_date = format_date(detail.start_date)
                end_date = format_date(detail.end_date)
        elif note.payment_code == '1019':
            detail = SubstitutionDetail.query.filter_by(service_note_id=note.id).first()
            if detail and detail.substitute:
                employee_name = detail.substitute.full_name
                amount = f"{detail.extra_pay_percent}%" if detail.extra_pay_percent else '-'
                start_date = format_date(detail.start_date)
                end_date = format_date(detail.end_date)
        elif note.payment_code == '9999':
            detail = AllowanceDetail.query.filter_by(service_note_id=note.id).first()
            if detail and detail.employee:
                employee_name = detail.employee.full_name
                amount = f"{detail.amount} {detail.unit or ''}"
                start_date = format_date(detail.start_date)
                end_date = format_date(detail.end_date)
        
        items.append({
            'id': note.id,
            'doc_number': note.doc_number or '-',
            'doc_date': format_date(note.doc_date),
            'payment_code': note.payment_code or '-',
            'employee_name': employee_name,
            'amount': str(amount),
            'start_date': start_date,
            'end_date': end_date
        })
    
    return jsonify({
        'items': items,
        'total': notes.total,
        'page': page,
        'pages': notes.pages,
        'per_page': per_page
    })

# ==================== API ДЛЯ СОТРУДНИКОВ ====================

@app.route('/api/employees')
def get_employees():
    employees = Employee.query.all()
    return jsonify([{
        'id': e.id,
        'tab_no': e.tab_no or '',
        'last_name': e.last_name or '',
        'first_name': e.first_name or '',
        'middle_name': e.middle_name or '',
        'department': e.department or '',
        'position': e.position or '',
        'position_code': e.position_code or ''
    } for e in employees])

@app.route('/api/employees/search')
def search_employees():
    query = request.args.get('q', '')
    if len(query) < 2:
        return jsonify([])
    
    employees = Employee.query.filter(
        (Employee.last_name.ilike(f'%{query}%')) |
        (Employee.first_name.ilike(f'%{query}%')) |
        (Employee.tab_no.ilike(f'%{query}%'))
    ).limit(20).all()
    
    return jsonify([{
        'id': e.id,
        'tab_no': e.tab_no,
        'full_name': e.full_name,
        'department': e.department,
        'position': e.position
    } for e in employees])

@app.route('/employees')
def employees_list():
    return render_template('employees.html')

# ==================== ПРОСМОТР СЛУЖЕБНОЙ ЗАПИСКИ ====================

@app.route('/note/<int:note_id>')
def view_note(note_id):
    note = ServiceNote.query.get_or_404(note_id)
    
    details = None
    if note.payment_code in ['1106', '1006']:
        details = SupplementDetail.query.filter_by(service_note_id=note_id).all()
    elif note.payment_code in ['1003', '1021', '1001']:
        details = CombinationDetail.query.filter_by(service_note_id=note_id).all()
    elif note.payment_code == '1019':
        details = SubstitutionDetail.query.filter_by(service_note_id=note_id).first()
    elif note.payment_code == '9999':
        details = AllowanceDetail.query.filter_by(service_note_id=note_id).all()
    
    return render_template('view_note.html', note=note, details=details)

# ==================== СТРАНИЦЫ ФОРМ ====================

@app.route('/note/new/<payment_code>')
def new_note(payment_code):
    return render_template(f'note_form_{payment_code}.html', payment_code=payment_code)

@app.route('/note/supplement', methods=['POST'])
def create_supplement_note():
    try:
        doc_number = request.form.get('doc_number')
        doc_date = datetime.strptime(request.form.get('doc_date'), '%Y-%m-%d').date()
        payment_code = request.form.get('payment_code')
        base_reason = request.form.get('base_reason')
        
        note = ServiceNote(doc_number=doc_number, doc_date=doc_date, payment_code=payment_code, base_reason=base_reason)
        db.session.add(note)
        db.session.flush()
        
        employee_ids = request.form.getlist('employee_id')
        start_dates = request.form.getlist('start_date')
        end_dates = request.form.getlist('end_date')
        amounts = request.form.getlist('amount')
        is_percents = request.form.getlist('is_percent')
        work_contents = request.form.getlist('work_content')
        
        for i in range(len(employee_ids)):
            if employee_ids[i]:
                detail = SupplementDetail(
                    service_note_id=note.id,
                    employee_id=int(employee_ids[i]),
                    start_date=datetime.strptime(start_dates[i], '%Y-%m-%d').date(),
                    end_date=datetime.strptime(end_dates[i], '%Y-%m-%d').date(),
                    amount=float(amounts[i]) if amounts[i] else 0,
                    is_percent=(is_percents[i] == 'on' if i < len(is_percents) else False),
                    work_content=work_contents[i] if i < len(work_contents) else ''
                )
                db.session.add(detail)
        
        db.session.commit()
        flash('Служебная записка создана!', 'success')
        return redirect(url_for('index'))
    except Exception as e:
        db.session.rollback()
        flash(f'Ошибка: {str(e)}', 'error')
        return redirect(request.referrer)

@app.route('/note/combination', methods=['POST'])
def create_combination_note():
    try:
        doc_number = request.form.get('doc_number')
        doc_date = datetime.strptime(request.form.get('doc_date'), '%Y-%m-%d').date()
        payment_code = request.form.get('payment_code')
        base_reason = request.form.get('base_reason')
        
        note = ServiceNote(doc_number=doc_number, doc_date=doc_date, payment_code=payment_code, base_reason=base_reason)
        db.session.add(note)
        db.session.flush()
        
        employee_ids = request.form.getlist('employee_id')
        combined_positions = request.form.getlist('combined_position')
        position_identifiers = request.form.getlist('position_identifier')
        departments = request.form.getlist('department')
        start_dates = request.form.getlist('start_date')
        end_dates = request.form.getlist('end_date')
        extra_pays = request.form.getlist('extra_pay')
        reasons = request.form.getlist('reason')
        
        for i in range(len(employee_ids)):
            if employee_ids[i]:
                detail = CombinationDetail(
                    service_note_id=note.id,
                    employee_id=int(employee_ids[i]),
                    combined_position=combined_positions[i] if i < len(combined_positions) else '',
                    position_identifier=position_identifiers[i] if i < len(position_identifiers) else '',
                    department=departments[i] if i < len(departments) else '',
                    start_date=datetime.strptime(start_dates[i], '%Y-%m-%d').date(),
                    end_date=datetime.strptime(end_dates[i], '%Y-%m-%d').date(),
                    extra_pay=float(extra_pays[i]) if extra_pays[i] else 0,
                    payment_code=payment_code,
                    reason=reasons[i] if i < len(reasons) else ''
                )
                db.session.add(detail)
        
        db.session.commit()
        flash('Служебная записка о совмещении создана!', 'success')
        return redirect(url_for('index'))
    except Exception as e:
        db.session.rollback()
        flash(f'Ошибка: {str(e)}', 'error')
        return redirect(request.referrer)

@app.route('/note/substitution', methods=['POST'])
def create_substitution_note():
    try:
        doc_number = request.form.get('doc_number')
        doc_date = datetime.strptime(request.form.get('doc_date'), '%Y-%m-%d').date()
        payment_code = request.form.get('payment_code')
        base_reason = request.form.get('base_reason')
        
        note = ServiceNote(doc_number=doc_number, doc_date=doc_date, payment_code=payment_code, base_reason=base_reason)
        db.session.add(note)
        db.session.flush()
        
        substitute_ids = request.form.getlist('substitute_employee_id')
        absent_ids = request.form.getlist('absent_employee_id')
        start_dates = request.form.getlist('start_date')
        end_dates = request.form.getlist('end_date')
        extra_pays = request.form.getlist('extra_pay_percent')
        duties_list = request.form.getlist('duties')
        absence_reasons = request.form.getlist('absence_reason')
        
        for i in range(len(substitute_ids)):
            if substitute_ids[i] and absent_ids[i]:
                detail = SubstitutionDetail(
                    service_note_id=note.id,
                    substitute_employee_id=int(substitute_ids[i]),
                    absent_employee_id=int(absent_ids[i]),
                    start_date=datetime.strptime(start_dates[i], '%Y-%m-%d').date(),
                    end_date=datetime.strptime(end_dates[i], '%Y-%m-%d').date(),
                    extra_pay_percent=float(extra_pays[i]) if extra_pays[i] else 20,
                    payment_code=payment_code,
                    duties=duties_list[i] if i < len(duties_list) else '',
                    absence_reason=absence_reasons[i] if i < len(absence_reasons) else ''
                )
                db.session.add(detail)
        
        db.session.commit()
        flash('Заявка на исполнение обязанностей создана!', 'success')
        return redirect(url_for('index'))
    except Exception as e:
        db.session.rollback()
        flash(f'Ошибка: {str(e)}', 'error')
        return redirect(request.referrer)

@app.route('/note/allowance', methods=['POST'])
def create_allowance_note():
    try:
        doc_number = request.form.get('doc_number')
        doc_date = datetime.strptime(request.form.get('doc_date'), '%Y-%m-%d').date()
        payment_code = request.form.get('payment_code')
        base_reason = request.form.get('base_reason')
        
        note = ServiceNote(doc_number=doc_number, doc_date=doc_date, payment_code=payment_code, base_reason=base_reason)
        db.session.add(note)
        db.session.flush()
        
        employee_ids = request.form.getlist('employee_id')
        allowance_names = request.form.getlist('allowance_name')
        units = request.form.getlist('unit')
        amounts = request.form.getlist('amount')
        statuses = request.form.getlist('status')
        start_dates = request.form.getlist('start_date')
        end_dates = request.form.getlist('end_date')
        
        # ИСПРАВЛЕНИЕ: Более надежный поиск кода ВО в форме
        detail_payment_code = request.form.get('allowance_payment_code') or request.form.get('payment_code') or ''
        
        for i in range(len(employee_ids)):
            if employee_ids[i]:
                detail = AllowanceDetail(
                    service_note_id=note.id,
                    employee_id=int(employee_ids[i]),
                    allowance_name=allowance_names[i] if i < len(allowance_names) else '',
                    payment_code=detail_payment_code,
                    unit=units[i] if i < len(units) else '',
                    amount=float(amounts[i]) if amounts[i] else 0,
                    status=statuses[i] if i < len(statuses) else 'установление',
                    start_date=datetime.strptime(start_dates[i], '%Y-%m-%d').date(),
                    end_date=datetime.strptime(end_dates[i], '%Y-%m-%d').date() if end_dates[i] else None
                )
                db.session.add(detail)
        
        db.session.commit()
        flash('Служебная записка об установлении/снятии доплаты создана!', 'success')
        return redirect(url_for('index'))
    except Exception as e:
        db.session.rollback()
        flash(f'Ошибка: {str(e)}', 'error')
        return redirect(request.referrer)

# ==================== ЭКСПОРТ В EXCEL ====================

@app.route('/export/1106/<int:note_id>')
def export_1106(note_id):
    try:
        note = ServiceNote.query.get_or_404(note_id)
        details = SupplementDetail.query.filter_by(service_note_id=note_id).all()
        
        template_path = os.path.join(TEMPLATE_DIR, 'v-9.7_rzo.xlsx')
        wb = load_workbook(template_path)
        ws = wb['СЗ']
        
        safe_write(ws, 4, 3, f"№ {note.doc_number} от {format_date(note.doc_date)}")
        
        start_row = 22
        
        for idx, detail in enumerate(details):
            emp = detail.employee
            row = start_row + idx
            seq = idx + 1
            
            safe_write(ws, row, 1, seq)
            safe_write(ws, row, 2, emp.tab_no if emp else '')
            safe_write(ws, row, 3, emp.full_name if emp else '')
            safe_write(ws, row, 4, emp.department if emp else '')
            safe_write(ws, row, 5, emp.position if emp else '')
            safe_write(ws, row, 6, format_date(detail.start_date))
            safe_write(ws, row, 7, format_date(detail.end_date))
            safe_write(ws, row, 8, detail.amount)
            safe_write(ws, row, 9, note.payment_code)
            safe_write(ws, row, 10, detail.work_content or '')
            safe_write(ws, row, 11, detail.work_type or '')
            
            if idx > 0:
                for col in range(1, 12):
                    copy_cell_style(ws.cell(row=start_row, column=col), ws.cell(row=row, column=col))

        output = BytesIO()
        wb.save(output)
        output.seek(0)
        return send_file(output, download_name=f'СЗ_В-9.7_{note.doc_number}.xlsx', as_attachment=True)
    except Exception as e:
        print(f"!!! ОШИБКА ЭКСПОРТА 1106: {e}")
        return f"Ошибка при формировании Excel: {e}", 500


@app.route('/export/1003/<int:note_id>')
def export_1003(note_id):
    try:
        note = ServiceNote.query.get_or_404(note_id)
        details = CombinationDetail.query.filter_by(service_note_id=note_id).all()
        
        template_path = os.path.join(TEMPLATE_DIR, 'v-9.4_comb.xlsx')
        wb = load_workbook(template_path)
        ws = wb['СЗ']
        
        safe_write(ws, 4, 5, note.doc_number or '') # E4
        safe_write(ws, 4, 6, format_date(note.doc_date)) # F4
        
        start_row = 20
        
        for idx, detail in enumerate(details):
            emp = detail.employee
            row = start_row + idx
            seq = idx + 1
            
            safe_write(ws, row, 1, seq)
            safe_write(ws, row, 2, emp.tab_no if emp else '')
            safe_write(ws, row, 3, emp.full_name if emp else '')
            safe_write(ws, row, 4, detail.combined_position or '')
            safe_write(ws, row, 5, detail.position_identifier or '')
            safe_write(ws, row, 6, detail.department or '')
            safe_write(ws, row, 7, format_date(detail.start_date))
            safe_write(ws, row, 8, format_date(detail.end_date))
            safe_write(ws, row, 9, f"{detail.extra_pay}%" if detail.extra_pay else '')
            safe_write(ws, row, 10, detail.payment_code or '')
            safe_write(ws, row, 14, detail.reason or '')
            safe_write(ws, row, 15, detail.duties or '')
            
            if idx > 0:
                for col in range(1, 17):
                    copy_cell_style(ws.cell(row=start_row, column=col), ws.cell(row=row, column=col))

        output = BytesIO()
        wb.save(output)
        output.seek(0)
        return send_file(output, download_name=f'СЗ_В-9.4_{note.doc_number}.xlsx', as_attachment=True)
    except Exception as e:
        print(f"!!! ОШИБКА ЭКСПОРТА 1003: {e}")
        return f"Ошибка при формировании Excel: {e}", 500


@app.route('/export/1019/<int:note_id>')
def export_1019(note_id):
    try:
        note = ServiceNote.query.get_or_404(note_id)
        detail = SubstitutionDetail.query.filter_by(service_note_id=note_id).first()
        
        template_path = os.path.join(TEMPLATE_DIR, 'v-9.10_subst.xlsx')
        wb = load_workbook(template_path)
        ws = wb['Заявка']
        
        safe_write(ws, 3, 15, note.doc_number or '') # O3
        safe_write(ws, 3, 16, format_date(note.doc_date)) # P3
        
        if detail:
            safe_write(ws, 4, 3, f"Заявка № {note.doc_number}") # C4
            safe_write(ws, 4, 8, format_date(note.doc_date)) # H4
            safe_write(ws, 6, 3, detail.absence_reason or '') # C6
            
            if detail.absent:
                safe_write(ws, 8, 1, detail.absent.full_name)
                safe_write(ws, 8, 9, detail.absent.tab_no)
                safe_write(ws, 10, 1, f"{detail.absent.position}, {detail.absent.department}")
                
            if detail.substitute:
                safe_write(ws, 14, 1, detail.substitute.full_name)
                safe_write(ws, 14, 9, detail.substitute.tab_no)
                safe_write(ws, 16, 1, f"{detail.substitute.position}, {detail.substitute.department}")
                
            safe_write(ws, 18, 2, format_date(detail.start_date))
            safe_write(ws, 18, 7, format_date(detail.end_date))

        output = BytesIO()
        wb.save(output)
        output.seek(0)
        return send_file(output, download_name=f'Заявка_В-9.10_{note.doc_number}.xlsx', as_attachment=True)
    except Exception as e:
        print(f"!!! ОШИБКА ЭКСПОРТА 1019: {e}")
        return f"Ошибка при формировании Excel: {e}", 500


@app.route('/export/9999/<int:note_id>')
def export_9999(note_id):
    try:
        note = ServiceNote.query.get_or_404(note_id)
        details = AllowanceDetail.query.filter_by(service_note_id=note_id).all()
        
        template_path = os.path.join(TEMPLATE_DIR, 'v-17.1_allow.xlsx')
        wb = load_workbook(template_path)
        ws = wb['СЗ']
        
        safe_write(ws, 2, 2, note.doc_number or '') # B2
        safe_write(ws, 2, 3, format_date(note.doc_date)) # C2
        
        start_row = 21
        
        for idx, detail in enumerate(details):
            emp = detail.employee
            row = start_row + idx
            seq = idx + 1
            
            safe_write(ws, row, 1, seq)
            safe_write(ws, row, 2, emp.tab_no if emp else '')
            safe_write(ws, row, 3, emp.full_name if emp else '')
            safe_write(ws, row, 4, detail.allowance_name or '')
            
            # ИСПРАВЛЕНИЕ: Берем код ВО строго из детали. Никаких fallback на 9999.
            safe_write(ws, row, 5, detail.payment_code or '') 
            
            safe_write(ws, row, 6, detail.unit or '')
            safe_write(ws, row, 7, detail.amount)
            safe_write(ws, row, 8, detail.status or 'установить')
            safe_write(ws, row, 9, format_date(detail.start_date))
            safe_write(ws, row, 10, format_date(detail.end_date))
            
            if idx > 0:
                for col in range(1, 11):
                    copy_cell_style(ws.cell(row=start_row, column=col), ws.cell(row=row, column=col))

        output = BytesIO()
        wb.save(output)
        output.seek(0)
        return send_file(output, download_name=f'СЗ_В-17.1_{note.doc_number}.xlsx', as_attachment=True)
    except Exception as e:
        print(f"!!! ОШИБКА ЭКСПОРТА 9999: {e}")
        return f"Ошибка при формировании Excel: {e}", 500


# ==================== ЗАПУСК ====================

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True, host='0.0.0.0', port=5000)