Skip to content

Commit 671fd65

Browse files
committed
Add Excel report generator tutorial code
1 parent aae8ff5 commit 671fd65

1 file changed

Lines changed: 283 additions & 0 deletions

File tree

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
"""
2+
Automated Sales Report Generator
3+
Produces a 4-sheet professional Excel report from raw sales data.
4+
5+
Usage:
6+
python sales_report_generator.py
7+
→ generates Sales_Report_Q1_2025.xlsx
8+
9+
Requirements:
10+
pip install openpyxl
11+
"""
12+
from openpyxl import Workbook
13+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
14+
from openpyxl.chart import BarChart, PieChart, Reference
15+
from openpyxl.chart.label import DataLabelList
16+
from openpyxl.chart.series import DataPoint
17+
from openpyxl.utils import get_column_letter
18+
from openpyxl.formatting.rule import DataBarRule, ColorScaleRule
19+
from collections import defaultdict
20+
21+
# ============================================================
22+
# CONFIGURATION — swap this with your own data source
23+
# ============================================================
24+
SALES_DATA = [
25+
# Product, Category, Region, Units, Price, Cost
26+
["Wireless Mouse", "Accessories", "North", 145, 24.99, 12.50],
27+
["Mechanical Keyboard", "Accessories", "North", 98, 89.99, 45.00],
28+
["USB-C Hub", "Accessories", "South", 210, 34.50, 17.25],
29+
["27\" Monitor", "Displays", "East", 45, 299.99, 180.00],
30+
["24\" Monitor", "Displays", "West", 67, 189.99, 110.00],
31+
["Webcam 1080p", "Peripherals", "North", 167, 54.99, 27.50],
32+
["Laptop Stand", "Accessories", "South", 320, 39.99, 15.00],
33+
["Desk Lamp LED", "Office", "East", 275, 29.99, 10.00],
34+
["Standing Desk", "Office", "West", 22, 499.99, 250.00],
35+
["Noise Canceling Phones", "Audio", "North", 89, 149.99, 75.00],
36+
["Bluetooth Speaker", "Audio", "South", 134, 79.99, 40.00],
37+
["HDMI Cable 6ft", "Accessories", "East", 450, 12.99, 4.00],
38+
["Wireless Charger", "Accessories", "West", 189, 19.99, 8.00],
39+
["Ergonomic Chair", "Office", "North", 15, 899.99, 450.00],
40+
]
41+
42+
# ============================================================
43+
# STYLES
44+
# ============================================================
45+
DARK_BLUE, MED_BLUE, LIGHT_BLUE = "1F4E79", "2E75B6", "D6E4F0"
46+
GREEN_HEADER, LIGHT_GREEN, WHITE = "375623", "E2EFDA", "FFFFFF"
47+
48+
hdr_fill = PatternFill(start_color=DARK_BLUE, end_color=DARK_BLUE, fill_type="solid")
49+
hdr_font = Font(name="Calibri", size=11, bold=True, color=WHITE)
50+
hdr_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
51+
thin_border = Border(left=Side(style="thin"), right=Side(style="thin"),
52+
top=Side(style="thin"), bottom=Side(style="thin"))
53+
alt_fill = PatternFill(start_color=LIGHT_BLUE, end_color=LIGHT_BLUE, fill_type="solid")
54+
curr_fmt, num_fmt, pct_fmt = '$#,##0.00', '#,##0', '0.0%'
55+
56+
# ============================================================
57+
# BUILD WORKBOOK
58+
# ============================================================
59+
wb = Workbook()
60+
61+
# ----- SHEET 1: Detailed Sales Data -----
62+
ws = wb.active
63+
ws.title = "Sales Data"
64+
65+
headers = ["Product", "Category", "Region", "Units Sold",
66+
"Unit Price", "Unit Cost", "Revenue", "Profit", "Margin %"]
67+
for c, h in enumerate(headers, 1):
68+
cell = ws.cell(row=1, column=c, value=h)
69+
cell.fill, cell.font, cell.alignment, cell.border = hdr_fill, hdr_font, hdr_align, thin_border
70+
71+
for r, row in enumerate(SALES_DATA, 2):
72+
for c, val in enumerate(row, 1):
73+
cell = ws.cell(row=r, column=c, value=val)
74+
cell.font = Font(name="Calibri", size=10)
75+
cell.border = thin_border
76+
if c >= 4:
77+
cell.alignment = Alignment(horizontal="right")
78+
if r % 2 == 0:
79+
cell.fill = alt_fill
80+
81+
# Formulas
82+
ws.cell(row=r, column=7, value=f"=D{r}*E{r}")
83+
ws.cell(row=r, column=8, value=f"=G{r}-(D{r}*F{r})")
84+
ws.cell(row=r, column=9, value=f"=H{r}/G{r}")
85+
ws.cell(row=r, column=5).number_format = curr_fmt
86+
ws.cell(row=r, column=6).number_format = curr_fmt
87+
ws.cell(row=r, column=7).number_format = curr_fmt
88+
ws.cell(row=r, column=8).number_format = curr_fmt
89+
ws.cell(row=r, column=9).number_format = pct_fmt
90+
91+
# Totals row
92+
tr = len(SALES_DATA) + 2
93+
ws.merge_cells(f"A{tr}:C{tr}")
94+
tc = ws.cell(row=tr, column=1, value="TOTALS")
95+
tc.font = Font(name="Calibri", size=11, bold=True, color=DARK_BLUE)
96+
tc.alignment = Alignment(horizontal="right")
97+
tc.fill = PatternFill(start_color=LIGHT_GREEN, end_color=LIGHT_GREEN, fill_type="solid")
98+
for col in [4, 7, 8]:
99+
cell = ws.cell(row=tr, column=col)
100+
cell.value = f"=SUM({get_column_letter(col)}2:{get_column_letter(col)}{tr - 1})"
101+
cell.font = Font(name="Calibri", size=11, bold=True)
102+
cell.fill = PatternFill(start_color=LIGHT_GREEN, end_color=LIGHT_GREEN, fill_type="solid")
103+
cell.border = thin_border
104+
cell.number_format = curr_fmt if col >= 7 else num_fmt
105+
106+
# Conditional formatting + freeze + auto-filter
107+
ws.conditional_formatting.add(
108+
f"I2:I{tr - 1}",
109+
ColorScaleRule(start_type="num", start_value=0, start_color="F8696B",
110+
mid_type="percentile", mid_value=50, mid_color="FFEB84",
111+
end_type="num", end_value=0.6, end_color="63BE7B"))
112+
ws.conditional_formatting.add(
113+
f"D2:D{tr - 1}",
114+
DataBarRule(start_type="min", end_type="max", color=MED_BLUE, showValue=True))
115+
ws.freeze_panes = "A2"
116+
ws.auto_filter.ref = f"A1:I{tr - 1}"
117+
118+
# Column widths
119+
for i, w in enumerate([26, 16, 10, 12, 14, 14, 14, 14, 12], 1):
120+
ws.column_dimensions[get_column_letter(i)].width = w
121+
122+
# ----- SHEET 2: Category Summary -----
123+
ws_cat = wb.create_sheet("Category Summary")
124+
125+
# Aggregate by category
126+
cat_data = defaultdict(lambda: {"units": 0, "revenue": 0.0, "profit": 0.0})
127+
for row in SALES_DATA:
128+
cat, units, price, cost = row[1], row[3], row[4], row[5]
129+
rev = units * price
130+
cat_data[cat]["units"] += units
131+
cat_data[cat]["revenue"] += rev
132+
cat_data[cat]["profit"] += rev - (units * cost)
133+
134+
sorted_cats = sorted(cat_data.items(), key=lambda x: x[1]["revenue"], reverse=True)
135+
136+
for c, h in enumerate(["Category", "Total Units", "Total Revenue", "Total Profit", "Margin %"], 1):
137+
cell = ws_cat.cell(row=1, column=c, value=h)
138+
cell.fill = PatternFill(start_color=GREEN_HEADER, end_color=GREEN_HEADER, fill_type="solid")
139+
cell.font = Font(name="Calibri", size=11, bold=True, color=WHITE)
140+
cell.alignment, cell.border = hdr_align, thin_border
141+
142+
for r, (cat, vals) in enumerate(sorted_cats, 2):
143+
ws_cat.cell(row=r, column=1, value=cat)
144+
ws_cat.cell(row=r, column=2, value=vals["units"])
145+
ws_cat.cell(row=r, column=3, value=round(vals["revenue"], 2))
146+
ws_cat.cell(row=r, column=4, value=round(vals["profit"], 2))
147+
ws_cat.cell(row=r, column=5, value=f"=D{r}/C{r}")
148+
for c in range(1, 6):
149+
cell = ws_cat.cell(row=r, column=c)
150+
cell.font = Font(name="Calibri", size=10)
151+
cell.border = thin_border
152+
if c >= 2:
153+
cell.alignment = Alignment(horizontal="right")
154+
if r % 2 == 0:
155+
cell.fill = alt_fill
156+
ws_cat.cell(row=r, column=3).number_format = curr_fmt
157+
ws_cat.cell(row=r, column=4).number_format = curr_fmt
158+
ws_cat.cell(row=r, column=5).number_format = pct_fmt
159+
160+
# Bar chart
161+
bar = BarChart()
162+
bar.type = "col"
163+
bar.style = 10
164+
bar.title = "Revenue by Product Category"
165+
bar.width, bar.height = 22, 14
166+
bar.add_data(Reference(ws_cat, min_col=3, min_row=1, max_row=len(sorted_cats) + 1),
167+
titles_from_data=True)
168+
bar.set_categories(Reference(ws_cat, min_col=1, min_row=2, max_row=len(sorted_cats) + 1))
169+
chart_colors = ["2E75B6", "ED7D31", "A5A5A5", "FFC000", "4472C4", "70AD47"]
170+
for i in range(len(sorted_cats)):
171+
pt = DataPoint(idx=i)
172+
pt.graphicalProperties.solidFill = chart_colors[i % 6]
173+
bar.series[0].data_points.append(pt)
174+
ws_cat.add_chart(bar, "G2")
175+
ws_cat.conditional_formatting.add(
176+
f"C2:C{len(sorted_cats) + 1}",
177+
DataBarRule(start_type="min", end_type="max", color=MED_BLUE, showValue=True))
178+
for i, w in enumerate([20, 14, 18, 16, 12], 1):
179+
ws_cat.column_dimensions[get_column_letter(i)].width = w
180+
181+
# ----- SHEET 3: Regional Breakdown -----
182+
ws_reg = wb.create_sheet("Regional Breakdown")
183+
reg_data = defaultdict(lambda: {"units": 0, "revenue": 0.0})
184+
for row in SALES_DATA:
185+
reg = row[2]
186+
units = row[3]
187+
rev = units * row[4]
188+
reg_data[reg]["units"] += units
189+
reg_data[reg]["revenue"] += rev
190+
191+
for c, h in enumerate(["Region", "Units Sold", "Revenue"], 1):
192+
cell = ws_reg.cell(row=1, column=c, value=h)
193+
cell.fill, cell.font, cell.alignment, cell.border = hdr_fill, hdr_font, hdr_align, thin_border
194+
195+
for r, (reg, vals) in enumerate(sorted(reg_data.items()), 2):
196+
ws_reg.cell(row=r, column=1, value=reg)
197+
ws_reg.cell(row=r, column=2, value=vals["units"])
198+
ws_reg.cell(row=r, column=3, value=round(vals["revenue"], 2))
199+
for c in range(1, 4):
200+
cell = ws_reg.cell(row=r, column=c)
201+
cell.font = Font(name="Calibri", size=10)
202+
cell.border = thin_border
203+
if c >= 2:
204+
cell.alignment = Alignment(horizontal="right")
205+
ws_reg.cell(row=r, column=3).number_format = curr_fmt
206+
207+
# Pie chart
208+
pie = PieChart()
209+
pie.title = "Revenue Share by Region"
210+
pie.width, pie.height = 18, 14
211+
pie.add_data(Reference(ws_reg, min_col=3, min_row=1, max_row=len(reg_data) + 1),
212+
titles_from_data=True)
213+
pie.set_categories(Reference(ws_reg, min_col=1, min_row=2, max_row=len(reg_data) + 1))
214+
pie.dataLabels = DataLabelList()
215+
pie.dataLabels.showPercent = True
216+
pie.dataLabels.showCatName = True
217+
for i in range(len(reg_data)):
218+
pt = DataPoint(idx=i)
219+
pt.graphicalProperties.solidFill = chart_colors[i % 6]
220+
pie.series[0].data_points.append(pt)
221+
ws_reg.add_chart(pie, "E2")
222+
for c, w in [('A', 14), ('B', 14), ('C', 14)]:
223+
ws_reg.column_dimensions[c].width = w
224+
225+
# ----- SHEET 4: Executive Dashboard -----
226+
ws_exec = wb.create_sheet("Executive Dashboard")
227+
ws_exec.merge_cells("A1:F1")
228+
title = ws_exec.cell(row=1, column=1, value="SALES PERFORMANCE DASHBOARD — Q1 2025")
229+
title.font = Font(name="Calibri", size=16, bold=True, color=DARK_BLUE)
230+
title.alignment = Alignment(horizontal="center", vertical="center")
231+
ws_exec.row_dimensions[1].height = 35
232+
233+
total_revenue = sum(r[3] * r[4] for r in SALES_DATA)
234+
total_units = sum(r[3] for r in SALES_DATA)
235+
total_profit = sum((r[3] * r[4]) - (r[3] * r[5]) for r in SALES_DATA)
236+
avg_margin = total_profit / total_revenue if total_revenue else 0
237+
238+
kpis = [("TOTAL REVENUE", f"${total_revenue:,.0f}", 1),
239+
("TOTAL UNITS SOLD", f"{total_units:,}", 2),
240+
("TOTAL PROFIT", f"${total_profit:,.0f}", 3),
241+
("AVG MARGIN", f"{avg_margin:.1%}", 4)]
242+
for kpi_title, kpi_val, col in kpis:
243+
for r_offset, (val, font) in enumerate(
244+
[(kpi_title, Font(size=10, bold=True, color=WHITE)),
245+
(kpi_val, Font(size=22, bold=True, color=WHITE))]):
246+
cell = ws_exec.cell(row=3 + r_offset, column=col, value=val)
247+
cell.font = font
248+
cell.fill = PatternFill(start_color=MED_BLUE, end_color=MED_BLUE, fill_type="solid")
249+
cell.alignment = Alignment(horizontal="center")
250+
ws_exec.column_dimensions[get_column_letter(col)].width = 22
251+
ws_exec.row_dimensions[4].height = 40
252+
253+
# Summary table on dashboard
254+
ws_exec.merge_cells("A6:D6")
255+
ws_exec.cell(row=6, column=1, value="Key Metrics by Category").font = Font(
256+
size=12, bold=True, color=DARK_BLUE)
257+
for c, h in enumerate(["Category", "Units", "Revenue", "Profit"], 1):
258+
cell = ws_exec.cell(row=7, column=c, value=h)
259+
cell.fill, cell.font, cell.alignment, cell.border = hdr_fill, hdr_font, hdr_align, thin_border
260+
for r, (cat, vals) in enumerate(sorted_cats, 8):
261+
ws_exec.cell(row=r, column=1, value=cat)
262+
ws_exec.cell(row=r, column=2, value=vals["units"])
263+
ws_exec.cell(row=r, column=3, value=round(vals["revenue"], 2))
264+
ws_exec.cell(row=r, column=4, value=round(vals["profit"], 2))
265+
for c in range(1, 5):
266+
cell = ws_exec.cell(row=r, column=c)
267+
cell.font = Font(name="Calibri", size=10)
268+
cell.border = thin_border
269+
if r % 2 == 0:
270+
cell.fill = alt_fill
271+
ws_exec.cell(row=r, column=3).number_format = curr_fmt
272+
ws_exec.cell(row=r, column=4).number_format = curr_fmt
273+
274+
# ============================================================
275+
# SAVE
276+
# ============================================================
277+
output_path = "Sales_Report_Q1_2025.xlsx"
278+
wb.save(output_path)
279+
print(f"✅ Report generated: {output_path}")
280+
print(f" Sheets: {wb.sheetnames}")
281+
print(f" Total Revenue: ${total_revenue:,.2f}")
282+
print(f" Total Profit: ${total_profit:,.2f}")
283+
print(f" Avg Margin: {avg_margin:.1%}")

0 commit comments

Comments
 (0)