One of the most time-consuming parts of freelance work isn't the work itself — it's reporting on it.
Every client wants updates. Every update takes time. Time you could spend billing.
Here's the Python system I built that generates client reports automatically, with zero manual formatting.
The Problem with Manual Reporting
Traditional approach:
- Open your project tracker
- Copy tasks to a doc
- Format it nicely
- Email it
- Repeat every week for every client
My time spent before automation: 2-3 hours every Friday.
After building this system: 4 minutes to review + click Send.
The Architecture
Three components:
- Data collector - pulls task status from wherever you track work
- Report generator - formats it into a professional narrative
- Email sender - delivers at the right time
Component 1: Data Collector
import json
from datetime import datetime, timedelta
from typing import Dict
class ProjectTracker:
def __init__(self, db_path='projects.json'):
self.db_path = db_path
try:
with open(db_path) as f:
self.data = json.load(f)
except FileNotFoundError:
self.data = {}
def get_week_summary(self, project_id: str, week_start: datetime) -> Dict:
project = self.data.get(project_id, {})
week_end = week_start + timedelta(days=7)
completed = []
in_progress = []
blocked = []
for task in project.get('tasks', []):
task_date = datetime.fromisoformat(task.get('updated', '2024-01-01'))
if task.get('status') == 'done' and week_start <= task_date <= week_end:
completed.append(task['title'])
elif task.get('status') == 'in_progress':
in_progress.append(task['title'])
elif task.get('status') == 'blocked':
blocked.append(task.get('blocker', task['title']))
return {
'project_name': project.get('name', 'Project'),
'client_name': project.get('client', 'Client'),
'client_email': project.get('email', ''),
'completed': completed,
'in_progress': in_progress,
'blocked': blocked,
}
def get_from_notion(self, database_id: str) -> Dict:
"""Pull tasks from Notion database"""
import httpx
headers = {
'Authorization': f'Bearer {NOTION_TOKEN}',
'Notion-Version': '2022-06-28'
}
week_ago = (datetime.now() - timedelta(days=7)).isoformat()
r = httpx.post(
f'https://api.notion.com/v1/databases/{database_id}/query',
headers=headers,
json={'filter': {'property': 'Last edited time', 'date': {'after': week_ago}}}
)
pages = r.json().get('results', [])
completed = [p['properties']['Name']['title'][0]['plain_text']
for p in pages
if p['properties'].get('Status', {}).get('select', {}).get('name') == 'Done']
return {'completed': completed}
Component 2: Report Generator
from datetime import datetime, timedelta
def generate_report(summary: dict) -> str:
client = summary['client_name']
project = summary['project_name']
completed = summary.get('completed', [])
in_progress = summary.get('in_progress', [])
blocked = summary.get('blocked', [])
today = datetime.now()
week_start = today - timedelta(days=7)
date_range = f"{week_start.strftime('%B %d')} – {today.strftime('%B %d, %Y')}"
report = f"Hi {client},\n\nHere's your weekly update for {project} ({date_range}).\n\n"
if completed:
report += "**Completed this week:**\n"
for item in completed:
report += f"✓ {item}\n"
report += "\n"
if in_progress:
report += "**Currently in progress:**\n"
for item in in_progress:
report += f"→ {item}\n"
report += "\n"
if blocked:
report += "**Needs your input:**\n"
for item in blocked:
report += f"⚠ {item}\n"
report += "\n"
next_week = in_progress[:3] if in_progress else ['Continuing project work']
report += "**Planned for next week:**\n"
for item in next_week:
report += f"• {item}\n"
report += "\nQuestions? Just reply to this email.\n\nBest regards"
return report
Component 3: Automated Delivery
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import schedule
import time
def send_report(recipient, project_name, report_text, sender_email, sender_password):
msg = MIMEMultipart('alternative')
msg['Subject'] = f"Weekly Update: {project_name} - {datetime.now().strftime('%B %d')}"
msg['From'] = sender_email
msg['To'] = recipient
text_part = MIMEText(report_text, 'plain')
html_body = report_text.replace('\n', '<br>').replace('✓', '✅').replace('→', '▶')
html_part = MIMEText(f"<html><body style='font-family:Arial;'>{html_body}</body></html>", 'html')
msg.attach(text_part)
msg.attach(html_part)
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
server.login(sender_email, sender_password)
server.sendmail(sender_email, recipient, msg.as_string())
print(f"Report sent to {recipient}")
# Schedule for every Friday at 4pm
schedule.every().friday.at("16:00").do(lambda: send_all_friday_reports())
Results After 3 Months
- Time on reporting per week: 3 hours → 4 minutes
- Client feedback: "Your updates are always so thorough and consistent"
- Missed reports: 0 (vs. occasionally forgetting before)
The automated reports look more professional than manual ones because they're consistently formatted every single week.
This reporting system is part of the Python Business Automation Toolkit — 47 ready-to-run scripts for freelancers and small business owners: https://lukassbrad.gumroad.com/l/ugeka
Top comments (0)