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:
- They report what the POS company thinks you need — not what you actually check every day
- They're slow — you log in, wait for the dashboard to load, click through 5 pages, and finally see yesterday's numbers
- 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:
- Total sales yesterday
- Sales by category (food, drinks, catering)
- Labor cost percentage
- Average check size
- Customer count
- Variance from target
- 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')
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"]
}
Setting Up the Daily Automation
# /etc/crontab - Run at 6 AM every day
0 6 * * * root /opt/restaurant_report/restaurant_daily_report.py
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 '📉'
}
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)
}
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
- It's YOUR report — you see exactly the numbers you check every morning, in the order you check them
- It arrives automatically — no login, no clicking, just an email at 6 AM
- It includes targets — you see variance, not just raw numbers
- It's free — $2,400/year stays in your pocket
- 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)