Подписывайтесь:

Блог AST-SoftPro

Генерация PDF-отчётов из данных: pandas + WeasyPrint для бизнеса

19.06.2026 9 мин чтения
Генерация PDF-отчётов из данных: pandas + WeasyPrint для бизнеса

Автоматическая генерация PDF-отчётов

PDF-отчёты — стандарт для бизнес-документации. Клиенты expect PDF, а не Excel. Python позволяет генерировать красивые отчёты с таблицами, графиками и логотипами автоматически из данных.

WeasyPrint конвертирует HTML+CSS в PDF. Это значит, что вы можете использовать знакомые HTML-шаблоны и CSS-стили для создания профессиональных документов.

Установка

pip install pandas weasyprint jinja2 matplotlib

Генерация отчёта из данных

import pandas as pd
from jinja2 import Template
from weasyprint import HTML
from datetime import datetime

# Данные для отчёта
sales_data = pd.DataFrame({
    'Месяц': ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн'],
    'Продажи': [120000, 150000, 180000, 165000, 210000, 240000],
    'Расходы': [80000, 90000, 95000, 100000, 110000, 120000],
})

# Вычисления
sales_data['Прибыль'] = sales_data['Продажи'] - sales_data['Расходы']
total_sales = sales_data['Продажи'].sum()
total_profit = sales_data['Прибыль'].sum()

# HTML-шаблон отчёта
REPORT_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <style>
        @page {
            size: A4;
            margin: 2cm;
            @bottom-center { content: "Страница " counter(page) " из " counter(pages); }
        }
        body { font-family: 'Helvetica', Arial, sans-serif; color: #333; }
        .header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #2c3e50; padding-bottom: 15px; }
        .company-name { font-size: 24px; font-weight: bold; color: #2c3e50; }
        .report-title { font-size: 18px; color: #7f8c8d; margin-top: 5px; }
        table { width: 100%; border-collapse: collapse; margin: 20px 0; }
        th { background: #3498db; color: white; padding: 10px; text-align: left; }
        td { padding: 8px; border-bottom: 1px solid #ddd; }
        .total-row { font-weight: bold; background: #ecf0f1; }
        .summary { display: flex; justify-content: space-between; margin: 20px 0; }
        .summary-box { background: #f8f9fa; padding: 15px; border-radius: 5px; width: 30%; text-align: center; }
        .summary-box h3 { margin: 0; color: #2c3e50; }
        .summary-box p { font-size: 24px; font-weight: bold; margin: 10px 0 0; }
        .positive { color: #27ae60; }
        .negative { color: #e74c3c; }
        .footer { text-align: center; font-size: 10px; color: #95a5a6; margin-top: 40px; }
    </style>
</head>
<body>
    <div class="header">
        <div class="company-name">{{ company_name }}</div>
        <div class="report-title">Финансовый отчёт за {{ period }}</div>
    </div>

    <div class="summary">
        <div class="summary-box">
            <h3>Продажи</h3>
            <p class="positive">{{ "{:,}".format(total_sales) }} ₽</p>
        </div>
        <div class="summary-box">
            <h3>Расходы</h3>
            <p class="negative">{{ "{:,}".format(total_expenses) }} ₽</p>
        </div>
        <div class="summary-box">
            <h3>Прибыль</h3>
            <p class="positive">{{ "{:,}".format(total_profit) }} ₽</p>
        </div>
    </div>

    <h2>Детализация по месяцам</h2>
    <table>
        <thead>
            <tr>
                <th>Месяц</th>
                <th>Продажи</th>
                <th>Расходы</th>
                <th>Прибыль</th>
            </tr>
        </thead>
        <tbody>
            {% for row in data %}
            <tr>
                <td>{{ row['Месяц'] }}</td>
                <td>{{ "{:,}".format(row["Продажи"]) }} ₽</td>
                <td>{{ "{:,}".format(row["Расходы"]) }} ₽</td>
                <td class="{{ 'positive' if row['Прибыль'] > 0 else 'negative' }}">
                    {{ "{:,}".format(row["Прибыль"]) }} ₽
                </td>
            </tr>
            {% endfor %}
            <tr class="total-row">
                <td>ИТОГО</td>
                <td>{{ "{:,}".format(total_sales) }} ₽</td>
                <td>{{ "{:,}".format(total_expenses) }} ₽</td>
                <td>{{ "{:,}".format(total_profit) }} ₽</td>
            </tr>
        </tbody>
    </table>

    <div class="footer">
        Сгенерировано автоматически {{ generated_at }} | {{ company_name }}
    </div>
</body>
</html>
'''

def generate_pdf_report(output_path='report.pdf'):
    template = Template(REPORT_TEMPLATE)
    html_content = template.render(
        company_name='ООО "Аналитика"',
        period='I полугодие 2026',
        total_sales=total_sales,
        total_expenses=sales_data['Расходы'].sum(),
        total_profit=total_profit,
        data=sales_data.to_dict('records'),
        generated_at=datetime.now().strftime('%d.%m.%Y %H:%M'),
    )

    HTML(string=html_content).write_pdf(output_path)
    print(f"✅ Отчёт сохранён: {output_path}")

generate_pdf_report()

Добавление графиков в отчёт

import matplotlib.pyplot as plt
import base64
from io import BytesIO

def create_chart_image():
    """Создание графика и возвращение base64 для вставки в HTML."""
    fig, ax = plt.subplots(figsize=(10, 5))
    ax.bar(sales_data['Месяц'], sales_data['Продажи'], label='Продажи', color='#3498db')
    ax.bar(sales_data['Месяц'], sales_data['Расходы'], label='Расходы', color='#e74c3c')
    ax.plot(sales_data['Месяц'], sales_data['Прибыль'], label='Прибыль', color='#27ae60', marker='o')
    ax.legend()
    ax.set_title('Финансовые показатели по месяцам')

    # Сохранение в base64 для вставки в HTML
    buf = BytesIO()
    plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
    buf.seek(0)
    img_base64 = base64.b64encode(buf.getvalue()).decode()
    plt.close()
    return img_base64

# В HTML-шаблоне добавить:
# <img src="data:image/png;base64,{{ chart_image }}" style="width:100%; margin: 20px 0;">

Автоматическая рассылка отчётов

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders

def send_report_email(recipient_email, pdf_path):
    """Отправка PDF-отчёта по email."""
    msg = MIMEMultipart()
    msg['Subject'] = f'Финансовый отчёт за {datetime.now().strftime("%B %Y")}'
    msg['From'] = 'reports@company.ru'
    msg['To'] = recipient_email

    body = f"""
    Уважаемый(ая) коллега!

    Направляем финансовый отчёт за {datetime.now().strftime('%B %Y')}.

    С уважением,
    Система автоматической отчётности
    """
    msg.attach(MIMEText(body, 'plain'))

    # Прикрепление PDF
    with open(pdf_path, 'rb') as f:
        attachment = MIMEBase('application', 'octet-stream')
        attachment.set_payload(f.read())
        encoders.encode_base64(attachment)
        attachment.add_header('Content-Disposition', f'attachment; filename=report_{datetime.now().strftime("%Y%m%d")}.pdf')
        msg.attach(attachment)

    server = smtplib.SMTP('smtp.gmail.com', 587)
    server.starttls()
    server.login('your_email@gmail.com', 'app_password')
    server.send_message(msg)
    server.quit()
    print(f"✅ Отчёт отправлен на {recipient_email}")

Сравнение библиотек для генерации PDF

Библиотека Подход Преимущества
WeasyPrint HTML → PDF CSS-стили, красивый дизайн, Unicode
ReportLab Программный Полный контроль, графики, подписи
pdfkit HTML → PDF (WebKit) Простой API, но требует Qt
FPDF Программный Лёгкая, но ограниченная

WeasyPrint — лучший выбор для бизнес-отчётов: вы пишете HTML/CSS, получаете PDF.

Заключение

Автоматическая генерация PDF-отчётов экономит часы ручной работы. Ключевые компоненты:

  • pandas для обработки данных и вычислений;

  • Jinja2 для HTML-шаблонов отчётов;

  • WeasyPrint для конвертации HTML в PDF;

  • matplotlib для графиков (опционально);

  • SMTP для автоматической рассылки.

Настройте расписание (APScheduler) и отчёты будут генерироваться и отправляться автоматически.


AI-Помощник