Lý thuyết
Bài 15/17

Automated Reports

Xây dựng hệ thống báo cáo tự động với Python

Automated Reports

Automated Reporting System

1. Introduction

Automated Reporting

Automated reports giảm manual work, đảm bảo consistency, và deliver insights đúng thời điểm. Analyst giỏi build systems, không phải chạy reports hàng ngày.

1.1 Why Automate?

Text
1┌──────────────────────────────────────────────────────────┐
2│ BENEFITS OF AUTOMATION │
3├──────────────────────────────────────────────────────────┤
4│ │
5│ TIME SAVINGS │
6│ ───────────────────────────────────────── │
7│ Before: 4 hours/week creating reports │
8│ After: 30 minutes setup, 10 min/week checking │
9│ Savings: 200+ hours/year │
10│ │
11│ CONSISTENCY │
12│ ───────────────────────────────────────── │
13│ • Same calculations every time │
14│ • No copy-paste errors │
15│ • Reproducible results │
16│ │
17│ TIMELINESS │
18│ ───────────────────────────────────────── │
19│ • Reports ready when stakeholders need them │
20│ • Real-time dashboards possible │
21│ • Alerts for anomalies │
22│ │
23│ SCALABILITY │
24│ ───────────────────────────────────────── │
25│ • Same effort for 10 or 1000 reports │
26│ • Easy to add new metrics │
27│ • Template-based expansion │
28│ │
29└──────────────────────────────────────────────────────────┘

1.2 Automation Architecture

Text
1┌─────────────────────────────────────────────────────────┐
2│ REPORTING AUTOMATION PIPELINE │
3├─────────────────────────────────────────────────────────┤
4│ │
5│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
6│ │ DATA │───>│ PROCESS │───>│ OUTPUT │ │
7│ │ SOURCE │ │ & CALC │ │ GENERATE │ │
8│ └──────────┘ └──────────┘ └──────────┘ │
9│ │ │ │
10│ v v │
11│ • Database • PDF/Excel │
12│ • APIs • HTML Email │
13│ • Files • Dashboard │
14│ • Streams • Slack/Teams │
15│ │
16│ ┌──────────────────────────────────────────────────┐ │
17│ │ SCHEDULER │ │
18│ │ • Cron / Task Scheduler │ │
19│ │ • Airflow / Dagster / Prefect │ │
20│ │ • Cloud Functions │ │
21│ └──────────────────────────────────────────────────┘ │
22│ │
23│ ┌──────────────────────────────────────────────────┐ │
24│ │ MONITORING │ │
25│ │ • Error alerts │ │
26│ │ • Run logging │ │
27│ │ • Performance tracking │ │
28│ └──────────────────────────────────────────────────┘ │
29│ │
30└─────────────────────────────────────────────────────────┘

2. Building Report Templates

2.1 Jinja2 Templates

Python
1from jinja2 import Template, Environment, FileSystemLoader
2import pandas as pd
3
4# Simple template
5simple_template = Template("""
6
7 {{ title }}
8 {{ date }}
9
10
11 KEY METRICS
12{% for metric in metrics %}
13{{ metric.status }} {{ metric.name }}: {{ metric.value }} (Target: {{ metric.target }})
14{% endfor %}
15
16 HIGHLIGHTS
17{% for highlight in highlights %}
18 {{ highlight }}
19{% endfor %}
20
21 ACTION ITEMS
22{% for action in actions %}
23{{ loop.index }}. {{ action.task }} (Owner: {{ action.owner }}, Due: {{ action.due }})
24{% endfor %}
25
26
27""")
28
29# Data for template
30data = {
31 'title': 'WEEKLY SALES REPORT',
32 'date': '2024-03-18',
33 'metrics': [
34 {'name': 'Revenue', 'value': '$2.5M', 'target': '$2.3M', 'status': '✅'},
35 {'name': 'Orders', 'value': '12,500', 'target': '11,000', 'status': '✅'},
36 {'name': 'Conversion', 'value': '3.2%', 'target': '3.5%', 'status': '⚠️'},
37 ],
38 'highlights': [
39 'Record Thursday revenue (+45% vs average)',
40 'Mobile app orders grew 25% MoM',
41 'Enterprise deal closed ($500K ACV)'
42 ],
43 'actions': [
44 {'task': 'Review mobile checkout UX', 'owner': 'Product', 'due': '3/22'},
45 {'task': 'Launch retargeting campaign', 'owner': 'Marketing', 'due': '3/20'},
46 ]
47}
48
49report = simple_template.render(**data)
50print(report)

2.2 HTML Email Templates

Python
1html_template = """
2<!DOCTYPE html>
3<html>
4<head>
5 <style>
6 body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; }
7 .header { background: #2c3e50; color: white; padding: 20px; text-align: center; }
8 .kpi-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; padding: 20px; }
9 .kpi-card { background: #f8f9fa; border-radius: 8px; padding: 20px; text-align: center; }
10 .kpi-value { font-size: 28px; font-weight: bold; color: #2c3e50; }
11 .kpi-label { color: #6c757d; margin-top: 5px; }
12 .status-green { color: #27ae60; }
13 .status-red { color: #e74c3c; }
14 .status-yellow { color: #f39c12; }
15 table { width: 100%; border-collapse: collapse; margin: 20px 0; }
16 th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
17 th { background: #ecf0f1; }
18 .section { padding: 20px; }
19 .section-title { font-size: 18px; font-weight: bold; margin-bottom: 15px; }
20 </style>
21</head>
22<body>
23 <div class="header">
24 <h1>{{ title }}</h1>
25 <p>{{ subtitle }}</p>
26 </div>
27
28 <div class="kpi-grid">
29 {% for kpi in kpis %}
30 <div class="kpi-card">
31 <div class="kpi-value {{ kpi.status_class }}">{{ kpi.value }}</div>
32 <div class="kpi-label">{{ kpi.name }}</div>
33 <div style="font-size: 12px; color: #888;">vs Target: {{ kpi.vs_target }}</div>
34 </div>
35 {% endfor %}
36 </div>
37
38 <div class="section">
39 <div class="section-title"> Performance Summary</div>
40 <table>
41 <tr>
42 <th>Metric</th>
43 <th>Current</th>
44 <th>Target</th>
45 <th>Variance</th>
46 <th>Trend</th>
47 </tr>
48 {% for row in performance_table %}
49 <tr>
50 <td>{{ row.metric }}</td>
51 <td>{{ row.current }}</td>
52 <td>{{ row.target }}</td>
53 <td class="{{ row.variance_class }}">{{ row.variance }}</td>
54 <td>{{ row.trend }}</td>
55 </tr>
56 {% endfor %}
57 </table>
58 </div>
59
60 <div class="section">
61 <div class="section-title"> Key Actions This Week</div>
62 <ol>
63 {% for action in actions %}
64 <li><strong>{{ action.task }}</strong> - {{ action.owner }} (Due: {{ action.due }})</li>
65 {% endfor %}
66 </ol>
67 </div>
68
69 <div style="background: #ecf0f1; padding: 15px; text-align: center; font-size: 12px;">
70 Auto-generated report | {{ timestamp }} | Questions? Contact analytics@company.com
71 </div>
72</body>
73</html>
74"""
75
76def generate_html_report(data):
77 """Generate HTML report from data"""
78 template = Template(html_template)
79 return template.render(**data)
80
81# Example data
82report_data = {
83 'title': 'Weekly Business Report',
84 'subtitle': 'Week of March 18, 2024',
85 'timestamp': '2024-03-18 08:00:00',
86 'kpis': [
87 {'name': 'Revenue', 'value': '$2.5M', 'vs_target': '+9%', 'status_class': 'status-green'},
88 {'name': 'Customers', 'value': '15,200', 'vs_target': '+8%', 'status_class': 'status-green'},
89 {'name': 'Churn Rate', 'value': '5.2%', 'vs_target': '-30%', 'status_class': 'status-red'}
90 ],
91 'performance_table': [
92 {'metric': 'Revenue', 'current': '$2.5M', 'target': '$2.3M', 'variance': '+8.7%', 'variance_class': 'status-green', 'trend': '📈'},
93 {'metric': 'Orders', 'current': '12,500', 'target': '11,000', 'variance': '+13.6%', 'variance_class': 'status-green', 'trend': '📈'},
94 {'metric': 'AOV', 'current': '$200', 'target': '$209', 'variance': '-4.3%', 'variance_class': 'status-yellow', 'trend': '📉'}
95 ],
96 'actions': [
97 {'task': 'Investigate churn increase', 'owner': 'CS Team', 'due': 'Mar 20'},
98 {'task': 'Launch upsell campaign', 'owner': 'Marketing', 'due': 'Mar 22'}
99 ]
100}
101
102html_report = generate_html_report(report_data)
103
104# Save HTML file
105with open('weekly_report.html', 'w', encoding='utf-8') as f:
106 f.write(html_report)
107print("✅ HTML report saved!")

2.3 Excel Reports

Python
1import pandas as pd
2from openpyxl import Workbook
3from openpyxl.styles import Font, Fill, PatternFill, Alignment, Border, Side
4from openpyxl.chart import BarChart, LineChart, Reference
5from openpyxl.utils.dataframe import dataframe_to_rows
6
7def create_excel_report(data, filename='report.xlsx'):
8 """Create formatted Excel report"""
9
10 wb = Workbook()
11
12 # Summary Sheet
13 ws_summary = wb.active
14 ws_summary.title = "Executive Summary"
15
16 # Styles
17 title_font = Font(size=16, bold=True, color="FFFFFF")
18 header_font = Font(size=12, bold=True)
19 title_fill = PatternFill(start_color="2C3E50", end_color="2C3E50", fill_type="solid")
20 green_fill = PatternFill(start_color="27AE60", end_color="27AE60", fill_type="solid")
21 red_fill = PatternFill(start_color="E74C3C", end_color="E74C3C", fill_type="solid")
22 yellow_fill = PatternFill(start_color="F39C12", end_color="F39C12", fill_type="solid")
23
24 thin_border = Border(
25 left=Side(style='thin'),
26 right=Side(style='thin'),
27 top=Side(style='thin'),
28 bottom=Side(style='thin')
29 )
30
31 # Title
32 ws_summary.merge_cells('A1:F1')
33 ws_summary['A1'] = 'WEEKLY BUSINESS REPORT'
34 ws_summary['A1'].font = title_font
35 ws_summary['A1'].fill = title_fill
36 ws_summary['A1'].alignment = Alignment(horizontal='center')
37
38 ws_summary['A2'] = f'Week of {data["date"]}'
39 ws_summary['A2'].font = Font(italic=True)
40
41 # KPI Summary
42 ws_summary['A4'] = 'KEY PERFORMANCE INDICATORS'
43 ws_summary['A4'].font = header_font
44
45 headers = ['KPI', 'Current', 'Target', 'Variance', 'Status']
46 for col, header in enumerate(headers, 1):
47 cell = ws_summary.cell(row=5, column=col, value=header)
48 cell.font = header_font
49 cell.border = thin_border
50
51 for row, kpi in enumerate(data['kpis'], 6):
52 ws_summary.cell(row=row, column=1, value=kpi['name']).border = thin_border
53 ws_summary.cell(row=row, column=2, value=kpi['current']).border = thin_border
54 ws_summary.cell(row=row, column=3, value=kpi['target']).border = thin_border
55
56 variance_cell = ws_summary.cell(row=row, column=4, value=kpi['variance'])
57 variance_cell.border = thin_border
58
59 status_cell = ws_summary.cell(row=row, column=5, value=kpi['status'])
60 status_cell.border = thin_border
61 status_cell.alignment = Alignment(horizontal='center')
62
63 if kpi['status'] == '✅':
64 status_cell.fill = green_fill
65 elif kpi['status'] == '❌':
66 status_cell.fill = red_fill
67 else:
68 status_cell.fill = yellow_fill
69
70 # Detailed Data Sheet
71 ws_detail = wb.create_sheet("Detailed Data")
72
73 # Add DataFrame
74 df = data['detailed_df']
75 for r_idx, row in enumerate(dataframe_to_rows(df, index=False, header=True), 1):
76 for c_idx, value in enumerate(row, 1):
77 cell = ws_detail.cell(row=r_idx, column=c_idx, value=value)
78 if r_idx == 1:
79 cell.font = header_font
80 cell.border = thin_border
81
82 # Chart Sheet
83 ws_chart = wb.create_sheet("Charts")
84
85 # Add trend data and chart
86 trend_data = data['trend_data']
87 for r_idx, row in enumerate(trend_data, 1):
88 for c_idx, value in enumerate(row, 1):
89 ws_chart.cell(row=r_idx, column=c_idx, value=value)
90
91 # Create chart
92 chart = LineChart()
93 chart.title = "Revenue Trend"
94 chart.y_axis.title = "Revenue ($M)"
95 chart.x_axis.title = "Week"
96
97 data_ref = Reference(ws_chart, min_col=2, min_row=1, max_col=2, max_row=len(trend_data))
98 categories = Reference(ws_chart, min_col=1, min_row=2, max_row=len(trend_data))
99
100 chart.add_data(data_ref, titles_from_data=True)
101 chart.set_categories(categories)
102 ws_chart.add_chart(chart, "D2")
103
104 # Adjust column widths
105 for ws in [ws_summary, ws_detail, ws_chart]:
106 for column in ws.columns:
107 max_length = 0
108 column_letter = column[0].column_letter
109 for cell in column:
110 try:
111 if len(str(cell.value)) > max_length:
112 max_length = len(str(cell.value))
113 except:
114 pass
115 ws.column_dimensions[column_letter].width = max_length + 2
116
117 wb.save(filename)
118 print(f"✅ Excel report saved: {filename}")
119
120# Sample data
121excel_data = {
122 'date': 'March 18, 2024',
123 'kpis': [
124 {'name': 'Revenue', 'current': '$2.5M', 'target': '$2.3M', 'variance': '+8.7%', 'status': '✅'},
125 {'name': 'Customers', 'current': '15,200', 'target': '14,000', 'variance': '+8.6%', 'status': '✅'},
126 {'name': 'Churn Rate', 'current': '5.2%', 'target': '4.0%', 'variance': '-30%', 'status': '❌'},
127 {'name': 'NPS', 'current': '48', 'target': '50', 'variance': '-4%', 'status': '⚠️'}
128 ],
129 'detailed_df': pd.DataFrame({
130 'Product': ['Product A', 'Product B', 'Product C'],
131 'Revenue': [1200000, 850000, 450000],
132 'Orders': [6000, 4500, 2000],
133 'Growth': ['15%', '8%', '-5%']
134 }),
135 'trend_data': [
136 ['Week', 'Revenue'],
137 ['Week 1', 2.1],
138 ['Week 2', 2.2],
139 ['Week 3', 2.3],
140 ['Week 4', 2.5]
141 ]
142}
143
144create_excel_report(excel_data, 'weekly_business_report.xlsx')

3. PDF Reports

3.1 Using ReportLab

Python
1from reportlab.lib import colors
2from reportlab.lib.pagesizes import A4
3from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
4from reportlab.lib.units import inch, cm
5from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
6from reportlab.graphics.shapes import Drawing
7from reportlab.graphics.charts.linecharts import HorizontalLineChart
8from reportlab.graphics.charts.barcharts import VerticalBarChart
9import io
10
11def create_pdf_report(data, filename='report.pdf'):
12 """Create professional PDF report"""
13
14 doc = SimpleDocTemplate(filename, pagesize=A4,
15 rightMargin=72, leftMargin=72,
16 topMargin=72, bottomMargin=72)
17
18 styles = getSampleStyleSheet()
19
20 # Custom styles
21 styles.add(ParagraphStyle(
22 name='ReportTitle',
23 parent=styles['Heading1'],
24 fontSize=24,
25 spaceAfter=30,
26 alignment=1 # Center
27 ))
28
29 styles.add(ParagraphStyle(
30 name='SectionTitle',
31 parent=styles['Heading2'],
32 fontSize=14,
33 spaceBefore=20,
34 spaceAfter=10,
35 textColor=colors.HexColor('#2C3E50')
36 ))
37
38 elements = []
39
40 # Title
41 elements.append(Paragraph("Weekly Business Report", styles['ReportTitle']))
42 elements.append(Paragraph(f"Week of {data['date']}", styles['Normal']))
43 elements.append(Spacer(1, 20))
44
45 # Executive Summary
46 elements.append(Paragraph("Executive Summary", styles['SectionTitle']))
47 elements.append(Paragraph(data['summary'], styles['Normal']))
48 elements.append(Spacer(1, 10))
49
50 # KPI Table
51 elements.append(Paragraph("Key Performance Indicators", styles['SectionTitle']))
52
53 kpi_data = [['KPI', 'Current', 'Target', 'Variance', 'Status']]
54 for kpi in data['kpis']:
55 kpi_data.append([kpi['name'], kpi['current'], kpi['target'], kpi['variance'], kpi['status']])
56
57 kpi_table = Table(kpi_data, colWidths=[2*inch, 1.2*inch, 1.2*inch, 1*inch, 0.8*inch])
58 kpi_table.setStyle(TableStyle([
59 ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2C3E50')),
60 ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
61 ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
62 ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
63 ('FONTSIZE', (0, 0), (-1, 0), 12),
64 ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
65 ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
66 ('GRID', (0, 0), (-1, -1), 1, colors.black)
67 ]))
68 elements.append(kpi_table)
69 elements.append(Spacer(1, 20))
70
71 # Key Highlights
72 elements.append(Paragraph("Key Highlights", styles['SectionTitle']))
73 for highlight in data['highlights']:
74 elements.append(Paragraph(f"• {highlight}", styles['Normal']))
75 elements.append(Spacer(1, 20))
76
77 # Action Items
78 elements.append(Paragraph("Action Items", styles['SectionTitle']))
79 action_data = [['#', 'Task', 'Owner', 'Due Date']]
80 for i, action in enumerate(data['actions'], 1):
81 action_data.append([str(i), action['task'], action['owner'], action['due']])
82
83 action_table = Table(action_data, colWidths=[0.4*inch, 3*inch, 1.2*inch, 1*inch])
84 action_table.setStyle(TableStyle([
85 ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#34495E')),
86 ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
87 ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
88 ('ALIGN', (0, 0), (0, -1), 'CENTER'),
89 ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
90 ('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
91 ]))
92 elements.append(action_table)
93
94 # Build PDF
95 doc.build(elements)
96 print(f"✅ PDF report saved: {filename}")
97
98# Sample data
99pdf_data = {
100 'date': 'March 18, 2024',
101 'summary': '''Revenue exceeded target by 8.7%, driven by strong performance in
102 the Enterprise segment. Customer acquisition remained healthy, though churn
103 rate increased to 5.2%, requiring immediate attention. Marketing efficiency
104 improved with CAC down 15% from previous month.''',
105 'kpis': [
106 {'name': 'Revenue', 'current': '$2.5M', 'target': '$2.3M', 'variance': '+8.7%', 'status': '✅'},
107 {'name': 'Customers', 'current': '15,200', 'target': '14,000', 'variance': '+8.6%', 'status': '✅'},
108 {'name': 'Churn Rate', 'current': '5.2%', 'target': '4.0%', 'variance': '-30%', 'status': '❌'},
109 {'name': 'NPS', 'current': '48', 'target': '50', 'variance': '-4%', 'status': '⚠️'}
110 ],
111 'highlights': [
112 'Record revenue day on Thursday (+45% vs typical)',
113 'Enterprise segment grew 35% MoM',
114 'Mobile app DAU reached 25,000 (+20%)'
115 ],
116 'actions': [
117 {'task': 'Launch churn prevention campaign', 'owner': 'CS Team', 'due': 'Mar 22'},
118 {'task': 'Review enterprise pricing strategy', 'owner': 'Finance', 'due': 'Mar 25'},
119 {'task': 'Optimize checkout conversion', 'owner': 'Product', 'due': 'Mar 28'}
120 ]
121}
122
123create_pdf_report(pdf_data, 'weekly_business_report.pdf')

4. Email Delivery

4.1 Sending Email Reports

Python
1import smtplib
2from email.mime.multipart import MIMEMultipart
3from email.mime.text import MIMEText
4from email.mime.base import MIMEBase
5from email import encoders
6import os
7
8class EmailReporter:
9 def __init__(self, smtp_server, smtp_port, username, password):
10 self.smtp_server = smtp_server
11 self.smtp_port = smtp_port
12 self.username = username
13 self.password = password
14
15 def send_report(self, to_emails, subject, html_content, attachments=None):
16 """Send email report with optional attachments"""
17
18 # Create message
19 msg = MIMEMultipart('alternative')
20 msg['From'] = self.username
21 msg['To'] = ', '.join(to_emails) if isinstance(to_emails, list) else to_emails
22 msg['Subject'] = subject
23
24 # Plain text fallback
25 text_content = "Please view this email in HTML format."
26 part1 = MIMEText(text_content, 'plain')
27 part2 = MIMEText(html_content, 'html')
28
29 msg.attach(part1)
30 msg.attach(part2)
31
32 # Add attachments
33 if attachments:
34 for filepath in attachments:
35 if os.path.exists(filepath):
36 with open(filepath, 'rb') as f:
37 part = MIMEBase('application', 'octet-stream')
38 part.set_payload(f.read())
39 encoders.encode_base64(part)
40 filename = os.path.basename(filepath)
41 part.add_header('Content-Disposition',
42 f'attachment; filename="{filename}"')
43 msg.attach(part)
44
45 # Send email
46 try:
47 with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
48 server.starttls()
49 server.login(self.username, self.password)
50 server.send_message(msg)
51 print(f"✅ Email sent to {to_emails}")
52 return True
53 except Exception as e:
54 print(f"❌ Failed to send email: {e}")
55 return False
56
57# Usage example (with environment variables for security)
58"""
59import os
60
61reporter = EmailReporter(
62 smtp_server='smtp.gmail.com',
63 smtp_port=587,
64 username=os.environ['EMAIL_USER'],
65 password=os.environ['EMAIL_PASSWORD']
66)
67
68reporter.send_report(
69 to_emails=['ceo@company.com', 'cfo@company.com'],
70 subject='Weekly Business Report - March 18, 2024',
71 html_content=html_report,
72 attachments=['weekly_report.xlsx', 'weekly_report.pdf']
73)
74"""

4.2 Slack/Teams Integration

Python
1import requests
2import json
3
4class SlackReporter:
5 def __init__(self, webhook_url):
6 self.webhook_url = webhook_url
7
8 def send_report(self, report_data):
9 """Send formatted report to Slack"""
10
11 # Build Slack message blocks
12 blocks = [
13 {
14 "type": "header",
15 "text": {
16 "type": "plain_text",
17 "text": f"📊 {report_data['title']}"
18 }
19 },
20 {
21 "type": "section",
22 "text": {
23 "type": "mrkdwn",
24 "text": f"*{report_data['subtitle']}*"
25 }
26 },
27 {"type": "divider"}
28 ]
29
30 # KPI section
31 kpi_text = "*Key Metrics:*\n"
32 for kpi in report_data['kpis']:
33 kpi_text += f"{kpi['status']} *{kpi['name']}*: {kpi['value']} (vs Target: {kpi['vs_target']})\n"
34
35 blocks.append({
36 "type": "section",
37 "text": {
38 "type": "mrkdwn",
39 "text": kpi_text
40 }
41 })
42
43 # Highlights
44 if report_data.get('highlights'):
45 blocks.append({"type": "divider"})
46 highlight_text = "*🎯 Key Highlights:*\n"
47 for h in report_data['highlights']:
48 highlight_text += f"• {h}\n"
49 blocks.append({
50 "type": "section",
51 "text": {"type": "mrkdwn", "text": highlight_text}
52 })
53
54 # Actions
55 if report_data.get('actions'):
56 blocks.append({"type": "divider"})
57 action_text = "*📋 Action Items:*\n"
58 for i, a in enumerate(report_data['actions'], 1):
59 action_text += f"{i}. {a['task']} _{a['owner']} - Due: {a['due']}_\n"
60 blocks.append({
61 "type": "section",
62 "text": {"type": "mrkdwn", "text": action_text}
63 })
64
65 # Send to Slack
66 payload = {"blocks": blocks}
67
68 try:
69 response = requests.post(
70 self.webhook_url,
71 json=payload,
72 headers={'Content-Type': 'application/json'}
73 )
74 if response.status_code == 200:
75 print("✅ Slack message sent")
76 return True
77 else:
78 print(f"❌ Slack error: {response.text}")
79 return False
80 except Exception as e:
81 print(f"❌ Error sending to Slack: {e}")
82 return False
83
84# Microsoft Teams Integration
85class TeamsReporter:
86 def __init__(self, webhook_url):
87 self.webhook_url = webhook_url
88
89 def send_report(self, report_data):
90 """Send report to Microsoft Teams"""
91
92 # Build Teams Adaptive Card
93 card = {
94 "@type": "MessageCard",
95 "@context": "http://schema.org/extensions",
96 "summary": report_data['title'],
97 "themeColor": "0076D7",
98 "title": f"📊 {report_data['title']}",
99 "sections": [{
100 "activityTitle": report_data['subtitle'],
101 "facts": [
102 {"name": kpi['name'], "value": f"{kpi['value']} ({kpi['vs_target']})"}
103 for kpi in report_data['kpis']
104 ],
105 "markdown": True
106 }]
107 }
108
109 if report_data.get('highlights'):
110 highlights_section = {
111 "title": "🎯 Key Highlights",
112 "text": "\n".join([f"• {h}" for h in report_data['highlights']])
113 }
114 card["sections"].append(highlights_section)
115
116 try:
117 response = requests.post(
118 self.webhook_url,
119 json=card,
120 headers={'Content-Type': 'application/json'}
121 )
122 if response.status_code == 200:
123 print("✅ Teams message sent")
124 return True
125 else:
126 print(f"❌ Teams error: {response.text}")
127 return False
128 except Exception as e:
129 print(f"❌ Error sending to Teams: {e}")
130 return False

5. Scheduling Reports

5.1 APScheduler (Python)

Python
1from apscheduler.schedulers.background import BackgroundScheduler
2from apscheduler.triggers.cron import CronTrigger
3from datetime import datetime
4import time
5
6def create_report_scheduler():
7 """Create scheduled report jobs"""
8
9 scheduler = BackgroundScheduler()
10
11 # Daily report at 7 AM
12 def daily_report():
13 print(f"📊 Running daily report at {datetime.now()}")
14 # Generate and send report
15 # generate_daily_report()
16 # send_report_email(recipients=['team@company.com'])
17
18 scheduler.add_job(
19 daily_report,
20 CronTrigger(hour=7, minute=0),
21 id='daily_report',
22 name='Daily Business Report'
23 )
24
25 # Weekly report every Monday at 8 AM
26 def weekly_report():
27 print(f"📊 Running weekly report at {datetime.now()}")
28 # generate_weekly_report()
29 # send_report_email(recipients=['leadership@company.com'])
30
31 scheduler.add_job(
32 weekly_report,
33 CronTrigger(day_of_week='mon', hour=8, minute=0),
34 id='weekly_report',
35 name='Weekly Business Report'
36 )
37
38 # Monthly report on the 1st at 9 AM
39 def monthly_report():
40 print(f"📊 Running monthly report at {datetime.now()}")
41 # generate_monthly_report()
42 # send_report_email(recipients=['executives@company.com'])
43
44 scheduler.add_job(
45 monthly_report,
46 CronTrigger(day=1, hour=9, minute=0),
47 id='monthly_report',
48 name='Monthly Business Report'
49 )
50
51 return scheduler
52
53# Usage
54scheduler = create_report_scheduler()
55scheduler.start()
56
57print("📅 Report scheduler started. Jobs:")
58for job in scheduler.get_jobs():
59 print(f" - {job.name}: {job.trigger}")
60
61# Keep running (in production, use proper process management)
62# try:
63# while True:
64# time.sleep(60)
65# except KeyboardInterrupt:
66# scheduler.shutdown()

5.2 GitHub Actions Workflow

yaml
1# .github/workflows/scheduled-reports.yml
2
3name: Scheduled Reports
4
5on:
6 schedule:
7 # Daily at 7 AM UTC
8 - cron: '0 7 * * *'
9 # Weekly Monday at 8 AM UTC
10 - cron: '0 8 * * 1'
11 workflow_dispatch: # Allow manual trigger
12
13jobs:
14 daily-report:
15 if: github.event.schedule == '0 7 * * *'
16 runs-on: ubuntu-latest
17 steps:
18 - uses: actions/checkout@v3
19
20 - name: Set up Python
21 uses: actions/setup-python@v4
22 with:
23 python-version: '3.10'
24
25 - name: Install dependencies
26 run: |
27 pip install -r requirements.txt
28
29 - name: Generate daily report
30 env:
31 DB_CONNECTION: ${{ secrets.DB_CONNECTION }}
32 EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }}
33 run: |
34 python scripts/generate_daily_report.py
35
36 - name: Upload report artifact
37 uses: actions/upload-artifact@v3
38 with:
39 name: daily-report
40 path: reports/daily_*.pdf
41
42 weekly-report:
43 if: github.event.schedule == '0 8 * * 1'
44 runs-on: ubuntu-latest
45 steps:
46 - uses: actions/checkout@v3
47
48 - name: Set up Python
49 uses: actions/setup-python@v4
50 with:
51 python-version: '3.10'
52
53 - name: Install dependencies
54 run: pip install -r requirements.txt
55
56 - name: Generate weekly report
57 env:
58 DB_CONNECTION: ${{ secrets.DB_CONNECTION }}
59 run: python scripts/generate_weekly_report.py
60
61 - name: Send to Slack
62 env:
63 SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
64 run: python scripts/send_slack_notification.py

6. Complete Reporting System

6.1 Full Pipeline Example

Python
1"""
2Complete Automated Reporting System
3===================================
4"""
5
6import pandas as pd
7import numpy as np
8from datetime import datetime, timedelta
9from sqlalchemy import create_engine
10from jinja2 import Template
11import smtplib
12from email.mime.multipart import MIMEMultipart
13from email.mime.text import MIMEText
14from email.mime.base import MIMEBase
15from email import encoders
16import os
17import logging
18
19# Setup logging
20logging.basicConfig(
21 level=logging.INFO,
22 format='%(asctime)s - %(levelname)s - %(message)s'
23)
24logger = logging.getLogger(__name__)
25
26class AutomatedReportingSystem:
27 """Complete automated reporting system"""
28
29 def __init__(self, config):
30 self.config = config
31 self.data = {}
32
33 def extract_data(self):
34 """Extract data from sources"""
35 logger.info("Extracting data...")
36
37 # Database connection (example)
38 # engine = create_engine(self.config['db_connection'])
39
40 # Simulate data extraction
41 self.data['revenue'] = pd.DataFrame({
42 'date': pd.date_range(start='2024-01-01', periods=90, freq='D'),
43 'revenue': np.random.uniform(70000, 100000, 90),
44 'orders': np.random.randint(300, 500, 90),
45 'customers': np.random.randint(250, 400, 90)
46 })
47
48 self.data['kpis'] = {
49 'revenue': {'current': 2500000, 'target': 2300000},
50 'customers': {'current': 15200, 'target': 14000},
51 'churn': {'current': 5.2, 'target': 4.0},
52 'nps': {'current': 48, 'target': 50}
53 }
54
55 logger.info("Data extraction complete")
56 return self.data
57
58 def calculate_metrics(self):
59 """Calculate all report metrics"""
60 logger.info("Calculating metrics...")
61
62 metrics = {}
63
64 # Revenue metrics
65 rev_df = self.data['revenue']
66 metrics['total_revenue'] = rev_df['revenue'].sum()
67 metrics['avg_daily_revenue'] = rev_df['revenue'].mean()
68 metrics['revenue_trend'] = rev_df['revenue'].pct_change().mean() * 100
69
70 # Customer metrics
71 metrics['total_orders'] = rev_df['orders'].sum()
72 metrics['avg_order_value'] = metrics['total_revenue'] / metrics['total_orders']
73
74 # KPI status
75 metrics['kpi_summary'] = []
76 for name, values in self.data['kpis'].items():
77 current = values['current']
78 target = values['target']
79
80 # Different logic for metrics where lower is better
81 if name in ['churn']:
82 variance = (target - current) / target * 100
83 on_track = current <= target
84 else:
85 variance = (current - target) / target * 100
86 on_track = current >= target
87
88 status = '✅' if on_track else '⚠️' if abs(variance) < 15 else '❌'
89
90 metrics['kpi_summary'].append({
91 'name': name.title(),
92 'current': current,
93 'target': target,
94 'variance': f"{'+' if variance > 0 else ''}{variance:.1f}%",
95 'status': status,
96 'on_track': on_track
97 })
98
99 self.metrics = metrics
100 logger.info("Metrics calculation complete")
101 return metrics
102
103 def generate_report(self, report_type='html'):
104 """Generate report in specified format"""
105 logger.info(f"Generating {report_type} report...")
106
107 if report_type == 'html':
108 return self._generate_html_report()
109 elif report_type == 'text':
110 return self._generate_text_report()
111 else:
112 raise ValueError(f"Unknown report type: {report_type}")
113
114 def _generate_text_report(self):
115 """Generate text report"""
116 template = Template("""
117
118 AUTOMATED BUSINESS REPORT
119 {{ date }}
120
121
122 KEY PERFORMANCE INDICATORS
123{% for kpi in kpis %}
124{{ kpi.status }} {{ kpi.name }}: {{ kpi.current }} (Target: {{ kpi.target }}, {{ kpi.variance }})
125{% endfor %}
126
127 REVENUE SUMMARY
128 Total Revenue: ${{ total_revenue | round(2) | format_number }}
129 Average Daily: ${{ avg_daily_revenue | round(2) | format_number }}
130 Trend: {{ revenue_trend | round(2) }}%
131
132 ORDER SUMMARY
133 Total Orders: {{ total_orders | format_number }}
134 Average Order Value: ${{ aov | round(2) }}
135
136
137Report generated automatically at {{ timestamp }}
138
139""")
140
141 # Custom filter for number formatting
142 def format_number(value):
143 return f"{value:,.0f}"
144
145 template.environment.filters['format_number'] = format_number
146
147 return template.render(
148 date=datetime.now().strftime('%Y-%m-%d'),
149 timestamp=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
150 kpis=self.metrics['kpi_summary'],
151 total_revenue=self.metrics['total_revenue'],
152 avg_daily_revenue=self.metrics['avg_daily_revenue'],
153 revenue_trend=self.metrics['revenue_trend'],
154 total_orders=self.metrics['total_orders'],
155 aov=self.metrics['avg_order_value']
156 )
157
158 def _generate_html_report(self):
159 """Generate HTML report"""
160 # Use more complex HTML template
161 template = Template("""
162<!DOCTYPE html>
163<html>
164<head>
165 <style>
166 body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
167 .header { background: #2c3e50; color: white; padding: 20px; text-align: center; border-radius: 8px; }
168 .kpi-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin: 20px 0; }
169 .kpi-card { background: #f8f9fa; border-radius: 8px; padding: 20px; text-align: center; }
170 .kpi-value { font-size: 24px; font-weight: bold; }
171 .on-track { color: #27ae60; }
172 .at-risk { color: #e74c3c; }
173 .warning { color: #f39c12; }
174 </style>
175</head>
176<body>
177 <div class="header">
178 <h1> Business Report</h1>
179 <p>{{ date }}</p>
180 </div>
181 <div class="kpi-grid">
182 {% for kpi in kpis %}
183 <div class="kpi-card">
184 <div class="kpi-value {% if kpi.on_track %}on-track{% else %}at-risk{% endif %}">
185 {{ kpi.current }}
186 </div>
187 <div>{{ kpi.name }}</div>
188 <div style="font-size: 12px; color: #888;">
189 Target: {{ kpi.target }} ({{ kpi.variance }})
190 </div>
191 </div>
192 {% endfor %}
193 </div>
194</body>
195</html>
196""")
197
198 return template.render(
199 date=datetime.now().strftime('%Y-%m-%d'),
200 kpis=self.metrics['kpi_summary']
201 )
202
203 def send_report(self, recipients, subject, content, attachments=None):
204 """Send report via email"""
205 logger.info(f"Sending report to {recipients}...")
206
207 # In production, implement actual email sending
208 # For now, simulate success
209 logger.info("✅ Report sent successfully")
210 return True
211
212 def run_pipeline(self, report_type='text', recipients=None):
213 """Run complete reporting pipeline"""
214 logger.info("=" * 50)
215 logger.info("Starting automated reporting pipeline")
216 logger.info("=" * 50)
217
218 try:
219 # Extract
220 self.extract_data()
221
222 # Transform/Calculate
223 self.calculate_metrics()
224
225 # Generate
226 report = self.generate_report(report_type)
227
228 # Deliver
229 if recipients:
230 self.send_report(
231 recipients=recipients,
232 subject=f"Business Report - {datetime.now().strftime('%Y-%m-%d')}",
233 content=report
234 )
235
236 logger.info("=" * 50)
237 logger.info("Pipeline completed successfully!")
238 logger.info("=" * 50)
239
240 return report
241
242 except Exception as e:
243 logger.error(f"Pipeline failed: {e}")
244 raise
245
246# Run the system
247config = {
248 'db_connection': 'postgresql://user:pass@localhost/db'
249}
250
251system = AutomatedReportingSystem(config)
252report = system.run_pipeline(report_type='text')
253print(report)

7. Thực hành

Build Your Report System

Exercise: Create Automated Report

💡 Xem đáp án
Python
1"""
2Exercise Solution: Build a custom automated report
3"""
4
5class MyReportSystem:
6 def __init__(self):
7 self.data = None
8 self.metrics = None
9
10 def load_data(self, filepath=None):
11 """Load data from file or generate sample"""
12 import pandas as pd
13 import numpy as np
14
15 # Sample data
16 np.random.seed(42)
17 dates = pd.date_range('2024-01-01', periods=30, freq='D')
18
19 self.data = pd.DataFrame({
20 'date': dates,
21 'revenue': np.random.uniform(80000, 120000, 30),
22 'orders': np.random.randint(400, 600, 30),
23 'new_customers': np.random.randint(50, 100, 30),
24 'returns': np.random.randint(10, 30, 30)
25 })
26
27 print(f"✅ Loaded {len(self.data)} rows")
28 return self
29
30 def calculate(self):
31 """Calculate all metrics"""
32 df = self.data
33
34 self.metrics = {
35 'total_revenue': df['revenue'].sum(),
36 'avg_revenue': df['revenue'].mean(),
37 'total_orders': df['orders'].sum(),
38 'avg_orders': df['orders'].mean(),
39 'total_new_customers': df['new_customers'].sum(),
40 'return_rate': df['returns'].sum() / df['orders'].sum() * 100,
41 'aov': df['revenue'].sum() / df['orders'].sum()
42 }
43
44 # Week over week comparison
45 week1 = df[df['date'] < '2024-01-15']['revenue'].sum()
46 week2 = df[df['date'] >= '2024-01-15']['revenue'].sum()
47 self.metrics['wow_growth'] = (week2 - week1) / week1 * 100
48
49 print("✅ Metrics calculated")
50 return self
51
52 def generate_report(self):
53 """Generate text report"""
54 m = self.metrics
55
56 report = f"""
57
58 MY AUTOMATED BUSINESS REPORT
59 {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M')}
60
61
62 REVENUE PERFORMANCE
63
64Total Revenue: ${m['total_revenue']:,.0f}
65Average Daily: ${m['avg_revenue']:,.0f}
66Week-over-Week: {'+' if m['wow_growth'] > 0 else ''}{m['wow_growth']:.1f}%
67
68 ORDER METRICS
69
70Total Orders: {m['total_orders']:,}
71Average Daily: {m['avg_orders']:.0f}
72Avg Order Value: ${m['aov']:.2f}
73
74 CUSTOMER METRICS
75
76New Customers: {m['total_new_customers']:,}
77Return Rate: {m['return_rate']:.1f}%
78
79
80Auto-generated by MyReportSystem
81
82"""
83 return report
84
85 def run(self):
86 """Run complete pipeline"""
87 self.load_data()
88 self.calculate()
89 report = self.generate_report()
90 print(report)
91 return report
92
93# Execute
94import pandas as pd # Needed in generate_report
95system = MyReportSystem()
96system.run()

8. Tổng kết

ComponentTools/Technologies
TemplatesJinja2, HTML, Markdown
Output FormatsPDF (ReportLab), Excel (openpyxl), HTML
DeliveryEmail (SMTP), Slack, Teams
SchedulingAPScheduler, Cron, GitHub Actions
PipelineExtract → Transform → Generate → Deliver

Bài tiếp theo: Portfolio Capstone