DEV Community

Brad
Brad

Posted on

How to Build a Client Reporting System with Python (That Clients Actually Love)

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:

  1. Open your project tracker
  2. Copy tasks to a doc
  3. Format it nicely
  4. Email it
  5. 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:

  1. Data collector - pulls task status from wherever you track work
  2. Report generator - formats it into a professional narrative
  3. 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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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)