Shop DataPythonPython 3.8+ · openpyxlMIT license
Merge machine CSV logs into a formatted Excel report (openpyxl)
The Friday ritual: someone opens a dozen per-machine CSV exports and pastes them into "the weekly sheet." This script is that ritual as a command: point it at the folder, it merges every CSV, skips malformed rows with a count instead of dying on them, and writes a workbook that looks like a report — bold headers, frozen top row, sized columns, and real =SUM() formulas Excel evaluates on open.
Before you run it
pip install openpyxl- CSV logs with the columns
machine, date, parts, scrap(extra columns are ignored)
The code
"""Merge a folder of per-machine CSV logs into one formatted Excel report.
Each CSV needs the columns: machine, date, parts, scrap (extras ignored).
Usage: python weekly_report.py logs/ --out weekly_production.xlsx
"""
import argparse
import csv
import sys
from pathlib import Path
from openpyxl import Workbook
from openpyxl.styles import Font
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("folder", help="folder containing the .csv logs")
ap.add_argument("--out", default="weekly_production.xlsx")
args = ap.parse_args()
files = sorted(Path(args.folder).glob("*.csv"))
if not files:
sys.exit(f"No .csv files found in {args.folder}")
wb = Workbook()
ws = wb.active
ws.title = "Production"
ws.append(["Machine", "Date", "Parts", "Scrap"])
for cell in ws[1]:
cell.font = Font(bold=True)
rows, skipped = 0, 0
for log in files:
with open(log, newline="") as fh:
for r in csv.DictReader(fh):
try:
ws.append([r["machine"], r["date"],
int(r["parts"]), int(r["scrap"])])
rows += 1
except (KeyError, ValueError):
skipped += 1 # malformed row: count it, don't die on it
# Totals row: real Excel formulas, evaluated when the file opens
last = ws.max_row + 2
ws[f"A{last}"] = "TOTAL"
ws[f"A{last}"].font = Font(bold=True)
ws[f"C{last}"] = f"=SUM(C2:C{last - 2})"
ws[f"D{last}"] = f"=SUM(D2:D{last - 2})"
ws.column_dimensions["A"].width = 16
ws.column_dimensions["B"].width = 12
ws.freeze_panes = "A2"
try:
wb.save(args.out)
except PermissionError:
sys.exit(f"Cannot write {args.out} - close it in Excel first.")
msg = f"{rows} rows from {len(files)} file(s) -> {args.out}"
if skipped:
msg += f" ({skipped} bad rows skipped)"
print(msg)
if __name__ == "__main__":
main()How it works
csv.DictReader+ atry/exceptper row is the whole robustness strategy: a machine log with a truncated last line costs you one row and a counter tick, not the report.- Formulas are written as strings (
=SUM(...)) — openpyxl doesn't calculate anything; Excel evaluates them on open. That's a feature: the totals stay live if someone edits a number. freeze_panes = "A2"and bold headers are the ten seconds of polish that decide whether people call it "the script output" or "the report."- The
PermissionErrorcatch turns the classic shared-drive failure (someone has the file open) into a one-line instruction instead of a traceback.
Gotchas & honest limits
- Column names are matched exactly (
machine, notMachine) — normalize your export or lowercase the keys if your logs vary. - openpyxl writes
.xlsxonly; ancient.xlsinputs need a one-time resave. - On a shared drive, someone always has the file open — hence the dated
--outfilename pattern being a good habit (--out report_2026-07-06.xlsx). - For analysis (grouping, filtering, joins) reach for pandas instead; this script is deliberately presentation-side.
Goes deeper
Want this adapted to your shop — or built into a real tool?
Samples are the free 80%. The last 20% is the part I do for a living.