/certificate_generator
  /static
    /images
      certificate_template.jpg (ваш шаблон сертификата)
  /templates
    index.html
    certificate.html
  app.py
  database.py

app.py:
from flask import Flask, render_template, request, send_file, send_from_directory
from datetime import datetime
import sqlite3
import os
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
import database

app = Flask(__name__)

# Инициализация БД
database.init_db()

# Конфигурация
CERTIFICATE_TEMPLATE = "static/images/certificate_template.png"
OUTPUT_FOLDER = "static/generated"
os.makedirs(OUTPUT_FOLDER, exist_ok=True)


def generate_certificate(first_name, last_name):
    # Генерация номера сертификата
    now = datetime.now()
    cert_number = f"CERT-{now.strftime('%Y%m%d')}-{len(database.get_all_certificates()) + 1:04d}"
    issue_date = now.strftime("%d.%m.%Y")

    # Загружаем шаблон
    img = Image.open(CERTIFICATE_TEMPLATE)
    if img.mode == 'RGBA':
        img = img.convert('RGB')

    draw = ImageDraw.Draw(img)

    # Настройки шрифтов
    try:
        name_font = ImageFont.truetype("arial.ttf", 52)
        number_font = ImageFont.truetype("arial.ttf", 50)
        date_font = ImageFont.truetype("arial.ttf", 30)
    except:
        name_font = ImageFont.load_default()
        number_font = ImageFont.load_default()
        date_font = ImageFont.load_default()

    # Полное имя
    full_name = f"{first_name} {last_name}"

    # Умное центрирование имени
    name_width = draw.textlength(full_name, font=name_font)
    center_x = 511  # Центральная точка из ваших настроек

    # Коррекция для длинных имен (эмпирическая формула)
    if len(full_name) >= 20:
        name_x = center_x - name_width * 0.27  # Для очень длинных имен

    elif len(full_name) > 18:
        name_x = center_x - name_width * 0.25  # Для длинных имен

    elif len(full_name) > 16:
        name_x = center_x - name_width * 0.23  # Для длинных имен

    elif len(full_name) > 14:
        name_x = center_x - name_width * 0.21  # Для длинных имен

    elif len(full_name) > 12:
        name_x = center_x - name_width * 0.15  # Для длинных имен
    elif len(full_name) > 7:
        name_x = center_x - name_width / 5  # Для коротких имен
    else:
        name_x = center_x - name_width / 7  # Для коротких имен

    name_y = 695  # Y-координата из настроек
    draw.text((name_x, name_y), full_name, fill="white", font=name_font)

    # Номер сертификата
    cert_text = f"№ {cert_number}"
    cert_width = draw.textlength(cert_text, font=number_font)
    cert_x = 390  # X-координата из настроек
    cert_y = img.height - 775  # Y от низа из настроек
    draw.text((cert_x, cert_y), cert_text, fill="white", font=number_font)

    date_of_issue = f"{now.strftime('%d.%m.%Y')}"
    date_x = 270
    date_y = img.height - 295
    draw.text((date_x, date_y), date_of_issue, fill="white", font=date_font)

    # Сохраняем сертификат
    output_path = os.path.join(OUTPUT_FOLDER, f"{cert_number}.jpg")
    img.save(output_path)

    return cert_number, issue_date, output_path.replace("\\", "/").replace("static/", "")


@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        first_name = request.form.get("first_name", "").strip()
        last_name = request.form.get("last_name", "").strip()
        phone = request.form.get("phone", "").strip()
        telegram = request.form.get("telegram", "").strip()
        force_create = request.form.get("force_create") == "true"

        if first_name and last_name:
            # Если пользователь нажал "Все равно создать новый"
            if force_create:
                cert_number, issue_date, cert_path = generate_certificate(first_name, last_name)
                database.add_certificate(first_name, last_name, phone, telegram, cert_number, issue_date)
                return render_template("certificate.html",
                                   certificate_path=cert_path.replace("static/", ""),
                                   certificate_number=cert_number)

            # Проверяем существование только при полном совпадении ВСЕХ данных
            if database.certificate_exists(first_name, last_name, phone or None, telegram or None):
                existing_cert = database.get_certificate_by_details(first_name, last_name, phone or None, telegram or None)
                return render_template("index.html",
                                   error="Сертификат для этих данных уже существует",
                                   existing_cert=existing_cert,
                                   first_name=first_name,
                                   last_name=last_name,
                                   phone=phone,
                                   telegram=telegram)

            # Создаем новый сертификат
            cert_number, issue_date, cert_path = generate_certificate(first_name, last_name)
            database.add_certificate(first_name, last_name, phone, telegram, cert_number, issue_date)

            return render_template("certificate.html",
                               certificate_path=cert_path.replace("static/", ""),
                               certificate_number=cert_number)

    return render_template("index.html")


@app.route("/download_excel")
def download_excel():
    from openpyxl import Workbook
    from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
    from openpyxl.utils import get_column_letter
    from io import BytesIO

    # Получаем данные
    certificates = database.get_all_certificates()

    # Создаем книгу Excel
    wb = Workbook()
    ws = wb.active
    ws.title = "Certificates"

    # Заголовки
    headers = ["ID", "Имя", "Фамилия", "Телефон", "Telegram", "№ Сертификата", "Дата выдачи"]
    ws.append(headers)

    # Стили для заголовков
    header_font = Font(bold=True, color="FFFFFF")
    header_fill = PatternFill(start_color="4F81BD", end_color="4F81BD", fill_type="solid")
    alignment = Alignment(horizontal="center", vertical="center")
    thin_border = Border(left=Side(style='thin'),
                         right=Side(style='thin'),
                         top=Side(style='thin'),
                         bottom=Side(style='thin'))

    # Применяем стили к заголовкам
    for col in range(1, len(headers) + 1):
        cell = ws.cell(row=1, column=col)
        cell.font = header_font
        cell.fill = header_fill
        cell.border = thin_border
        cell.alignment = alignment

    # Добавляем данные
    for cert in certificates:
        ws.append(cert)

    # Стили для данных
    data_font = Font(color="000000")
    data_fill_even = PatternFill(start_color="DCE6F1", end_color="DCE6F1", fill_type="solid")
    data_fill_odd = PatternFill(start_color="E9E9E9", end_color="E9E9E9", fill_type="solid")

    # Применяем стили к данным
    for row in range(2, ws.max_row + 1):
        for col in range(1, ws.max_column + 1):
            cell = ws.cell(row=row, column=col)
            cell.font = data_font
            cell.border = thin_border
            cell.alignment = Alignment(horizontal="left", vertical="center")

            # Чередуем цвета строк для лучшей читаемости
            if row % 2 == 0:
                cell.fill = data_fill_even
            else:
                cell.fill = data_fill_odd

    # Автоматически подбираем ширину столбцов
    for col in ws.columns:
        max_length = 0
        column = col[0].column_letter  # Получаем букву столбца

        for cell in col:
            try:
                if len(str(cell.value)) > max_length:
                    max_length = len(str(cell.value))
            except:
                pass

        # Добавляем небольшой отступ
        adjusted_width = (max_length + 2) * 1.2
        ws.column_dimensions[column].width = adjusted_width

    # Сохраняем в BytesIO
    output = BytesIO()
    wb.save(output)
    output.seek(0)

    return send_file(
        output,
        as_attachment=True,
        download_name="certificates_export.xlsx",
        mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )


if __name__ == "__main__":
    app.run(debug=True)

Как мне сконвертировать эту программу в exe, при запуске exe должен открываться сайт
http://127.0.0.1:5000/

Очень важно, чтобы создались все папки при запуске exe, но папки должны быть не пустыми Нужно подтянуть файл static/images/certificate_template.png, поскольку на основе него создаётся сертификат.