Code Library
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

GitHub
"""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 + a try/except per 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 PermissionError catch 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, not Machine) — normalize your export or lowercase the keys if your logs vary.
  • openpyxl writes .xlsx only; ancient .xls inputs need a one-time resave.
  • On a shared drive, someone always has the file open — hence the dated --out filename 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.

Get in touch
Home
Blog
Tools
Code
Email
LinkedIn
Résumé