DEV Community

Hive80-lab
Hive80-lab

Posted on

The Restaurant Owner's Guide to Daily Sales Reporting (Without Expensive Software)

The Restaurant Owner's Guide to Daily Sales Reporting (Without Expensive Software)

Most restaurant POS systems charge $200/month for reports you can generate with a 50-line Python script.

I helped a family-owned restaurant switch from a $200/month reporting SaaS to a custom script that produces better reports. They saved $2,400/year and got reports that actually match their workflow.

Here's how to build your own daily sales reporting system in under an hour.

The Problem With Restaurant POS Reports

Most POS reporting tools have three issues:

  1. They report what the POS company thinks you need — not what you actually check every day
  2. They're slow — you log in, wait for the dashboard to load, click through 5 pages, and finally see yesterday's numbers
  3. They don't compare to your targets — you see raw numbers but not whether you're on track

What Restaurant Owners Actually Need

Every restaurant owner I've worked with checks the same 7 numbers every morning:

  1. Total sales yesterday
  2. Sales by category (food, drinks, catering)
  3. Labor cost percentage
  4. Average check size
  5. Customer count
  6. Variance from target
  7. Week-over-week comparison

The Solution: A Custom Daily Report

#!/usr/bin/env python3
"""restaurant_daily_report.py
Generates a daily sales report for restaurant owners.
Reads from POS export CSV (most POS systems can export daily data).
"""
import csv
import json
from datetime import datetime, timedelta
import smtplib
from email.mime.text import MIMEText

class RestaurantReport:
    def __init__(self, config_file='restaurant_config.json'):
        with open(config_file) as f:
            self.config = json.load(f)

    def load_sales_data(self, csv_file):
        """Load POS export data"""
        sales = []
        with open(csv_file) as f:
            reader = csv.DictReader(f)
            for row in reader:
                sales.append({
                    'date': row.get('Date', ''),
                    'category': row.get('Category', 'Food'),
                    'item': row.get('Item', ''),
                    'quantity': int(row.get('Qty', 0)),
                    'price': float(row.get('Price', 0)),
                    'total': float(row.get('Total', 0))
                })
        return sales

    def calculate_metrics(self, sales):
        """Calculate the 7 key metrics"""
        total_sales = sum(s['total'] for s in sales)

        # Sales by category
        categories = {}
        for s in sales:
            cat = s['category']
            categories[cat] = categories.get(cat, 0) + s['total']

        # Average check
        transactions = len(set(s.get('transaction_id', i) for i, s in enumerate(sales)))
        avg_check = total_sales / transactions if transactions > 0 else 0

        # Labor cost (from config or separate input)
        labor_cost = self.config.get('daily_labor_cost', 0)
        labor_pct = (labor_cost / total_sales * 100) if total_sales > 0 else 0

        # Target comparison
        target = self.config.get('daily_sales_target', 0)
        variance = total_sales - target
        variance_pct = (variance / target * 100) if target > 0 else 0

        return {
            'date': datetime.now().strftime('%Y-%m-%d'),
            'total_sales': round(total_sales, 2),
            'categories': {k: round(v, 2) for k, v in categories.items()},
            'avg_check': round(avg_check, 2),
            'customer_count': transactions,
            'labor_cost': labor_cost,
            'labor_pct': round(labor_pct, 1),
            'target': target,
            'variance': round(variance, 2),
            'variance_pct': round(variance_pct, 1),
            'on_track': variance >= 0
        }

    def format_report(self, metrics):
        """Format as a clean text report"""
        status = '✅ ON TRACK' if metrics['on_track'] else '⚠️ BEHIND TARGET'

        report = f"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  DAILY SALES REPORT — {metrics['date']}
  {status}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  TOTAL SALES:      ${metrics['total_sales']:,.2f}
  TARGET:           ${metrics['target']:,.2f}
  VARIANCE:         ${metrics['variance']:+,.2f} ({metrics['variance_pct']:+.1f}%)

  SALES BY CATEGORY:
"""
        for cat, amount in metrics['categories'].items():
            pct = (amount / metrics['total_sales'] * 100) if metrics['total_sales'] > 0 else 0
            report += f"  {cat:15s} ${amount:>10,.2f} ({pct:.1f}%)\n"

        report += f"""
  KEY METRICS:
  Average Check:    ${metrics['avg_check']:.2f}
  Customer Count:   {metrics['customer_count']}
  Labor Cost:       ${metrics['labor_cost']:,.2f} ({metrics['labor_pct']:.1f}% of sales)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
        return report

    def send_email(self, report, to_email):
        """Email the report to the owner"""
        msg = MIMEText(report)
        msg['Subject'] = f'Daily Sales Report — {datetime.now().strftime("%Y-%m-%d")}'
        msg['From'] = self.config.get('email_from', 'reports@restaurant.com')
        msg['To'] = to_email

        # Send via SMTP
        smtp = smtplib.SMTP(self.config.get('smtp_host', 'localhost'))
        smtp.send_message(msg)
        smtp.quit()

# Usage
if __name__ == '__main__':
    reporter = RestaurantReport()
    sales = reporter.load_sales_data('pos_export.csv')
    metrics = reporter.calculate_metrics(sales)
    report = reporter.format_report(metrics)
    print(report)

    # Email to owner
    reporter.send_email(report, 'owner@restaurant.com')
Enter fullscreen mode Exit fullscreen mode

The Configuration File

{
  "restaurant_name": "Mario's Italian Kitchen",
  "daily_sales_target": 3500,
  "daily_labor_cost": 850,
  "email_from": "reports@marioskitchen.com",
  "smtp_host": "smtp.gmail.com",
  "categories": ["Food", "Drinks", "Catering", "Merchandise"]
}
Enter fullscreen mode Exit fullscreen mode

Setting Up the Daily Automation

# /etc/crontab - Run at 6 AM every day
0 6 * * * root /opt/restaurant_report/restaurant_daily_report.py
Enter fullscreen mode Exit fullscreen mode

The owner gets a clean email every morning at 6 AM with yesterday's numbers. No login required. No dashboard to navigate. Just the 7 numbers that matter.

Week-Over-Week Comparison

def week_comparison(self, today_metrics, last_week_metrics):
    wow_change = today_metrics['total_sales'] - last_week_metrics['total_sales']
    wow_pct = (wow_change / last_week_metrics['total_sales'] * 100) \
              if last_week_metrics['total_sales'] > 0 else 0

    return {
        'this_week': today_metrics['total_sales'],
        'last_week': last_week_metrics['total_sales'],
        'change': round(wow_change, 2),
        'change_pct': round(wow_pct, 1),
        'trend': '📈' if wow_change > 0 else '📉'
    }
Enter fullscreen mode Exit fullscreen mode

Monthly Summary

def monthly_summary(self, daily_reports):
    total_sales = sum(r['total_sales'] for r in daily_reports)
    total_labor = sum(r['labor_cost'] for r in daily_reports)
    avg_daily = total_sales / len(daily_reports)
    best_day = max(daily_reports, key=lambda r: r['total_sales'])
    worst_day = min(daily_reports, key=lambda r: r['total_sales'])

    return {
        'month': datetime.now().strftime('%B %Y'),
        'total_sales': round(total_sales, 2),
        'total_labor': round(total_labor, 2),
        'labor_pct': round(total_labor / total_sales * 100, 1),
        'avg_daily_sales': round(avg_daily, 2),
        'best_day': best_day['date'],
        'best_day_sales': best_day['total_sales'],
        'worst_day': worst_day['date'],
        'worst_day_sales': worst_day['total_sales'],
        'days_on_target': sum(1 for r in daily_reports if r['on_track']),
        'total_days': len(daily_reports)
    }
Enter fullscreen mode Exit fullscreen mode

The ROI

Metric POS SaaS Custom Script
Monthly cost $200 $0
Setup time 2 hours 1 hour
Customization Limited Full
Report speed 30 seconds Instant
Data ownership Vendor You
Annual savings $2,400

Why This Works Better

  1. It's YOUR report — you see exactly the numbers you check every morning, in the order you check them
  2. It arrives automatically — no login, no clicking, just an email at 6 AM
  3. It includes targets — you see variance, not just raw numbers
  4. It's free — $2,400/year stays in your pocket
  5. You own the data — no vendor lock-in, no API limits, no price increases

Want the complete restaurant reporting toolkit? The Restaurant Daily Sales Report Template includes the full Python script, configuration templates, email formatting, and POS export guides — everything you need to replace your $200/month reporting SaaS.

What's the first number you check every morning at your restaurant?

Top comments (0)