Chapter 6: Make the Flask Report Useful

Adding cleaner layout, summaries, charts, and Excel download to your web report

Chapter Goal

From Web Demo to Practical Reporting App

Chapter 5 gave the report a browser form. That was a major step because users no longer needed to run commands. But the app still felt basic.

In this chapter, we improve the Flask app so it starts feeling like something you could share with another developer or demo to a business user.

FormUser selects filters
SummaryShow record counts
TableDisplay filtered data
ChartVisual pattern
DownloadExport Excel
Big idea: A useful tool does not only show data. It helps the user understand, trust, and reuse the result.

What We Are Building

We will keep the same material report, but add four practical improvements:

πŸ“Œ
Summary Cards

Total filtered records, total stock, and selected filters.

πŸ“Š
Simple Chart

A visual count by material type or plant.

⬇️
Excel Download

A browser button to download filtered results.

Expected browser preview

Material Report App
Generate Report Download Excel
2Filtered Records
112Total Stock
FERT / 1000Selected Filters
MaterialDescriptionTypePlantStock
1006Demo Finished Product 3FERT100067
1001Demo Finished ProductFERT100045

Updated Project Structure

We will add a folder for generated reports and a static CSS file. This keeps the app cleaner.

think-like-python-flask-report/ β”‚ β”œβ”€β”€ app.py β”œβ”€β”€ materials.csv β”œβ”€β”€ requirements.txt β”‚ β”œβ”€β”€ generated/ β”‚ └── filtered_materials.xlsx ← created by the app β”‚ β”œβ”€β”€ static/ β”‚ └── style.css β”‚ └── templates/ └── index.html
ABAP comparison: This is like moving from one giant report include into a cleaner structure: program logic, layout, output files, and reusable styling.

Step 1: Add the Generated Folder

The app needs a safe place to store the Excel file before downloading it.

generated/

We will also make the app create this folder automatically if it does not exist.

import os GENERATED_FOLDER = "generated" os.makedirs(GENERATED_FOLDER, exist_ok=True)

It makes the app more forgiving. If another developer downloads your project and forgets to create the folder, the app still works.

Step 2: Add a Download Route

A Flask route can return a page, but it can also return a file. This is how we create the Excel download button.

from flask import send_file @app.route("/download") def download(): file_path = os.path.join(GENERATED_FOLDER, "filtered_materials.xlsx") return send_file(file_path, as_attachment=True)
Beginner caution: This simple version assumes the Excel file already exists. Later, you can add checks so users get a friendly message if they try to download before generating a report.

Step 3: Add Summary Metrics

Summary metrics help the user quickly understand the report without scanning every row.

Python summary logic

record_count = len(filtered_df) total_stock = int(filtered_df["stock"].sum()) summary = { "record_count": record_count, "total_stock": total_stock, "selected_filters": f"{material_type} / {plant}" }

Expected visual result

Summary Cards
2Filtered Records
112Total Stock
FERT / 1000Selected Filters

In many business reports, the first question is not β€œwhat are all the rows?” It is β€œhow many records are we talking about?” or β€œdoes this output look reasonable?” Summary cards help establish that trust quickly.

Step 4: Add a Simple HTML Chart

In a full app, you might use Bokeh, Plotly, Chart.js, or another visualization library. For this beginner web chapter, we will keep the chart simple and render basic bars using HTML and CSS.

chart_data = ( filtered_df["mtart"] .value_counts() .reset_index() ) chart_data.columns = ["label", "value"] chart_records = chart_data.to_dict(orient="records")

Expected chart preview

Filtered Material Count by Type
2
FERT
Why not Bokeh here? We already introduced Bokeh earlier. This chapter focuses on Flask mechanics: routes, templates, downloads, and passing data to HTML.

Full Updated app.py

Replace your previous app.py with this version.

import os from flask import Flask, render_template, request, send_file import pandas as pd app = Flask(__name__) DATA_FILE = "materials.csv" GENERATED_FOLDER = "generated" EXCEL_FILE = "filtered_materials.xlsx" os.makedirs(GENERATED_FOLDER, exist_ok=True) @app.route("/", methods=["GET", "POST"]) def index(): df = pd.read_csv(DATA_FILE) material_type = "FERT" plant = "1000" records = [] chart_records = [] summary = None message = "" if request.method == "POST": material_type = request.form.get("material_type", "FERT") plant = request.form.get("plant", "1000") filtered_df = df[ (df["mtart"] == material_type) & (df["plant"].astype(str) == plant) ] filtered_df = filtered_df.sort_values(by="stock", ascending=False) if filtered_df.empty: message = "No records found for the selected filters." else: records = filtered_df.to_dict(orient="records") summary = { "record_count": len(filtered_df), "total_stock": int(filtered_df["stock"].sum()), "selected_filters": f"{material_type} / {plant}" } chart_data = ( filtered_df["mtart"] .value_counts() .reset_index() ) chart_data.columns = ["label", "value"] chart_records = chart_data.to_dict(orient="records") output_path = os.path.join(GENERATED_FOLDER, EXCEL_FILE) filtered_df.to_excel(output_path, index=False) message = f"Found {len(records)} record(s)." return render_template( "index.html", material_type=material_type, plant=plant, records=records, chart_records=chart_records, summary=summary, message=message ) @app.route("/download") def download(): file_path = os.path.join(GENERATED_FOLDER, EXCEL_FILE) if not os.path.exists(file_path): return "No Excel file has been generated yet. Please run a report first." return send_file(file_path, as_attachment=True) if __name__ == "__main__": app.run(debug=True)
  • We added send_file for Excel downloads.
  • We added a generated folder for output files.
  • We calculated summary metrics.
  • We prepared chart data for the template.
  • We added a /download route.

Full Updated templates/index.html

Replace your previous templates/index.html with this version.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Material Report</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="container"> <h1>Material Report</h1> <p>Select your filters and generate the report.</p> <form method="POST"> <label>Material Type</label> <select name="material_type"> <option value="FERT">FERT</option> <option value="HALB">HALB</option> <option value="ROH">ROH</option> <option value="HAWA">HAWA</option> </select> <label>Plant</label> <input name="plant" value="{{ plant }}"> <button type="submit">Generate Report</button> </form> {% if message %} <div class="message">{{ message }}</div> {% endif %} {% if summary %} <div class="summary-grid"> <div class="summary-card"> <strong>{{ summary.record_count }}</strong> <span>Filtered Records</span> </div> <div class="summary-card"> <strong>{{ summary.total_stock }}</strong> <span>Total Stock</span> </div> <div class="summary-card"> <strong>{{ summary.selected_filters }}</strong> <span>Selected Filters</span> </div> </div> <p> <a class="download-button" href="/download">Download Excel</a> </p> {% endif %} {% if chart_records %} <h2>Material Count by Type</h2> <div class="chart-box"> {% for item in chart_records %} <div class="bar-row"> <span class="bar-label">{{ item.label }}</span> <div class="bar-track"> <div class="bar-fill" style="width: {{ item.value * 25 }}%;"> {{ item.value }} </div> </div> </div> {% endfor %} </div> {% endif %} {% if records %} <h2>Filtered Records</h2> <table> <tr> <th>Material</th> <th>Description</th> <th>Type</th> <th>Plant</th> <th>Stock</th> </tr> {% for row in records %} <tr> <td>{{ row.matnr }}</td> <td>{{ row.maktx }}</td> <td>{{ row.mtart }}</td> <td>{{ row.plant }}</td> <td>{{ row.stock }}</td> </tr> {% endfor %} </table> {% endif %} </div> </body> </html>

url_for('static', filename='style.css') asks Flask to build the correct URL for the CSS file. This is better than hardcoding a path.

Add static/style.css

Create a folder named static, then create a file named style.css.

body { font-family: Arial, sans-serif; background: #f4f6f8; margin: 0; padding: 30px; color: #2d3748; } .container { max-width: 1000px; margin: auto; background: white; padding: 25px; border-radius: 12px; } h1, h2 { color: #0b1f3a; } label { display: block; margin-top: 12px; font-weight: bold; } input, select { width: 100%; padding: 10px; margin-top: 4px; } button, .download-button { display: inline-block; margin-top: 18px; background: #13a89e; color: white; border: none; padding: 10px 16px; border-radius: 8px; font-weight: bold; text-decoration: none; } .message { margin-top: 18px; background: #ecfeff; border-left: 5px solid #06b6d4; padding: 12px; } .summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 20px; } .summary-card { background: #f8fafc; border: 1px solid #dbeafe; border-radius: 12px; padding: 14px; } .summary-card strong { display: block; color: #0b1f3a; font-size: 1.4rem; } .chart-box { margin-top: 12px; } .bar-row { display: grid; grid-template-columns: 90px 1fr; gap: 12px; align-items: center; margin-bottom: 10px; } .bar-track { background: #e2e8f0; border-radius: 999px; overflow: hidden; } .bar-fill { background: #13a89e; color: white; padding: 6px 10px; min-width: 35px; } table { width: 100%; border-collapse: collapse; margin-top: 22px; } th, td { border: 1px solid #dbeafe; padding: 9px; text-align: left; } th { background: #e8eef7; color: #0b1f3a; }
Why separate CSS? It keeps your Python logic and page design from becoming tangled together.

Run and Test

Start the app:

python app.py

Open:

http://127.0.0.1:5000

Test path

OpenLocal URL
SubmitFERT / 1000
ReviewSummary + table
DownloadExcel file
VerifyOpen workbook
What success looks like
  • The browser shows summary cards.
  • The browser shows a simple chart.
  • The table shows filtered rows.
  • The Download Excel button downloads filtered_materials.xlsx.
  • The terminal does not show errors.

Common Beginner Issues

Confirm the file is located here:

static/style.css

Also confirm this line exists in the HTML head:

<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">

Generate a report first. The Excel file is created only after the app has filtered data.

In this beginner version, bar width is calculated with a simple multiplier:

style="width: {{ item.value * 25 }}%;"

Later, you can normalize values so chart widths scale more accurately.

Try This

Add this in app.py:

from datetime import datetime generated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

Then pass it into render_template() and display it in the HTML.

Chapter 6 Checkpoint

Next: Chapter 7 can prepare this app for cloud deployment by introducing environment variables, requirements, a Procfile, and a Cloud Foundry-ready structure.
End of Chapter 6