@nomad-e/bluma-cli 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -12
- package/dist/config/native_tools.json +7 -0
- package/dist/config/skills/git-commit/LICENSE.txt +18 -0
- package/dist/config/skills/git-commit/SKILL.md +258 -0
- package/dist/config/skills/git-commit/references/REFERENCE.md +249 -0
- package/dist/config/skills/git-commit/scripts/validate_commit_msg.py +163 -0
- package/dist/config/skills/git-pr/LICENSE.txt +18 -0
- package/dist/config/skills/git-pr/SKILL.md +293 -0
- package/dist/config/skills/git-pr/references/REFERENCE.md +256 -0
- package/dist/config/skills/git-pr/scripts/validate_commits.py +112 -0
- package/dist/config/skills/pdf/LICENSE.txt +26 -0
- package/dist/config/skills/pdf/SKILL.md +327 -0
- package/dist/config/skills/pdf/references/FORMS.md +69 -0
- package/dist/config/skills/pdf/references/REFERENCE.md +52 -0
- package/dist/config/skills/pdf/scripts/create_report.py +59 -0
- package/dist/config/skills/pdf/scripts/merge_pdfs.py +39 -0
- package/dist/config/skills/skill-creator/LICENSE.txt +26 -0
- package/dist/config/skills/skill-creator/SKILL.md +229 -0
- package/dist/config/skills/xlsx/LICENSE.txt +18 -0
- package/dist/config/skills/xlsx/SKILL.md +298 -0
- package/dist/config/skills/xlsx/references/REFERENCE.md +337 -0
- package/dist/config/skills/xlsx/scripts/office/__init__.py +2 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/__init__.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/pack.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/soffice.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/unpack.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/__pycache__/validate.cpython-312.pyc +0 -0
- package/dist/config/skills/xlsx/scripts/office/pack.py +58 -0
- package/dist/config/skills/xlsx/scripts/office/soffice.py +180 -0
- package/dist/config/skills/xlsx/scripts/office/unpack.py +63 -0
- package/dist/config/skills/xlsx/scripts/office/validate.py +122 -0
- package/dist/config/skills/xlsx/scripts/recalc.py +143 -0
- package/dist/main.js +275 -89
- package/package.json +1 -1
- package/dist/config/example.bluma-mcp.json.txt +0 -14
- package/dist/config/models_config.json +0 -78
- package/dist/skills/git-conventional/LICENSE.txt +0 -3
- package/dist/skills/git-conventional/SKILL.md +0 -83
- package/dist/skills/skill-creator/SKILL.md +0 -495
- package/dist/skills/testing/LICENSE.txt +0 -3
- package/dist/skills/testing/SKILL.md +0 -114
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xlsx
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill any time a spreadsheet file is the primary input or output.
|
|
5
|
+
Triggers: open, read, edit, create, fix, or convert .xlsx, .xlsm, .csv,
|
|
6
|
+
or .tsv files — including adding columns, computing formulas, formatting,
|
|
7
|
+
charting, cleaning messy data, restructuring tabular data, or building
|
|
8
|
+
financial models. Also trigger when the user references a spreadsheet by
|
|
9
|
+
name or path and wants something done to it. The deliverable must be a
|
|
10
|
+
spreadsheet file. Do NOT trigger when the primary deliverable is a Word
|
|
11
|
+
document, HTML report, or standalone script.
|
|
12
|
+
license: Proprietary. LICENSE.txt has complete terms
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# XLSX — Spreadsheet Creation, Editing & Analysis
|
|
16
|
+
|
|
17
|
+
## Core Principle
|
|
18
|
+
|
|
19
|
+
> **Formulas, not hardcodes.** Every calculated value MUST be an Excel
|
|
20
|
+
> formula so the spreadsheet stays dynamic. Never compute in Python and
|
|
21
|
+
> paste the result into a cell.
|
|
22
|
+
|
|
23
|
+
## Output Quality Standards
|
|
24
|
+
|
|
25
|
+
### Professional Appearance
|
|
26
|
+
- Consistent font throughout (Arial or Calibri, 10-11pt)
|
|
27
|
+
- Headers bold with subtle background fill
|
|
28
|
+
- Column widths auto-fitted to content
|
|
29
|
+
- Number formatting applied to all numeric cells
|
|
30
|
+
- Borders and alignment used consistently
|
|
31
|
+
|
|
32
|
+
### Zero Formula Errors
|
|
33
|
+
Every deliverable MUST have ZERO formula errors. After writing formulas,
|
|
34
|
+
always run `scripts/recalc.py` and fix any errors before delivering.
|
|
35
|
+
|
|
36
|
+
### Preserve Existing Templates
|
|
37
|
+
When modifying an existing file, study and EXACTLY match its format,
|
|
38
|
+
style, and conventions. Existing patterns override these guidelines.
|
|
39
|
+
|
|
40
|
+
## Reading & Analyzing Data
|
|
41
|
+
|
|
42
|
+
### Quick Analysis with pandas
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import pandas as pd
|
|
46
|
+
|
|
47
|
+
df = pd.read_excel('file.xlsx')
|
|
48
|
+
all_sheets = pd.read_excel('file.xlsx', sheet_name=None)
|
|
49
|
+
|
|
50
|
+
df.head()
|
|
51
|
+
df.info()
|
|
52
|
+
df.describe()
|
|
53
|
+
|
|
54
|
+
df.to_excel('output.xlsx', index=False)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Reading with openpyxl (preserves formulas)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from openpyxl import load_workbook
|
|
61
|
+
|
|
62
|
+
wb = load_workbook('file.xlsx')
|
|
63
|
+
ws = wb.active
|
|
64
|
+
|
|
65
|
+
for row in ws.iter_rows(min_row=1, max_row=5, values_only=True):
|
|
66
|
+
print(row)
|
|
67
|
+
|
|
68
|
+
wb_data = load_workbook('file.xlsx', data_only=True)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Warning**: Never open with `data_only=True` and save — formulas are
|
|
72
|
+
permanently replaced with cached values.
|
|
73
|
+
|
|
74
|
+
## Creating New Excel Files
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from openpyxl import Workbook
|
|
78
|
+
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
79
|
+
from openpyxl.utils import get_column_letter
|
|
80
|
+
|
|
81
|
+
wb = Workbook()
|
|
82
|
+
ws = wb.active
|
|
83
|
+
ws.title = "Data"
|
|
84
|
+
|
|
85
|
+
header_font = Font(bold=True, size=11, name='Arial')
|
|
86
|
+
header_fill = PatternFill('solid', fgColor='D9E1F2')
|
|
87
|
+
header_align = Alignment(horizontal='center', vertical='center')
|
|
88
|
+
|
|
89
|
+
headers = ['Item', 'Qty', 'Unit Price', 'Total']
|
|
90
|
+
for col, h in enumerate(headers, 1):
|
|
91
|
+
cell = ws.cell(row=1, column=col, value=h)
|
|
92
|
+
cell.font = header_font
|
|
93
|
+
cell.fill = header_fill
|
|
94
|
+
cell.alignment = header_align
|
|
95
|
+
|
|
96
|
+
ws['D2'] = '=B2*C2'
|
|
97
|
+
|
|
98
|
+
for col in range(1, len(headers) + 1):
|
|
99
|
+
ws.column_dimensions[get_column_letter(col)].width = 15
|
|
100
|
+
|
|
101
|
+
wb.save('output.xlsx')
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Editing Existing Files
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from openpyxl import load_workbook
|
|
108
|
+
|
|
109
|
+
wb = load_workbook('existing.xlsx')
|
|
110
|
+
ws = wb['SheetName']
|
|
111
|
+
|
|
112
|
+
ws['A1'] = 'New Value'
|
|
113
|
+
ws.insert_rows(2)
|
|
114
|
+
ws.delete_cols(3)
|
|
115
|
+
|
|
116
|
+
new_ws = wb.create_sheet('Summary')
|
|
117
|
+
new_ws['A1'] = '=SUM(Data!B2:B100)'
|
|
118
|
+
|
|
119
|
+
wb.save('modified.xlsx')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Formula Rules
|
|
123
|
+
|
|
124
|
+
### Always Use Excel Formulas
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# WRONG — hardcoded calculation
|
|
128
|
+
total = sum(values)
|
|
129
|
+
ws['B10'] = total
|
|
130
|
+
|
|
131
|
+
# CORRECT — Excel formula
|
|
132
|
+
ws['B10'] = '=SUM(B2:B9)'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This applies to ALL calculations: totals, averages, percentages,
|
|
136
|
+
growth rates, ratios, conditional aggregations.
|
|
137
|
+
|
|
138
|
+
### Common Formula Patterns
|
|
139
|
+
|
|
140
|
+
| Operation | Formula |
|
|
141
|
+
|-----------|---------|
|
|
142
|
+
| Sum | `=SUM(B2:B9)` |
|
|
143
|
+
| Average | `=AVERAGE(B2:B9)` |
|
|
144
|
+
| Count non-empty | `=COUNTA(B2:B9)` |
|
|
145
|
+
| Percentage | `=B2/B$10` |
|
|
146
|
+
| Year-over-year growth | `=(C2-B2)/B2` |
|
|
147
|
+
| Conditional sum | `=SUMIF(A:A,"Category",B:B)` |
|
|
148
|
+
| Lookup | `=VLOOKUP(A2,Data!A:C,3,FALSE)` |
|
|
149
|
+
| If/then | `=IF(B2>0,B2*C2,0)` |
|
|
150
|
+
| Error handling | `=IFERROR(B2/C2,0)` |
|
|
151
|
+
|
|
152
|
+
### Cross-Sheet References
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
ws['A1'] = "=Summary!B5"
|
|
156
|
+
ws['A2'] = "=VLOOKUP(A1,Data!A:C,3,FALSE)"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Recalculating Formulas (MANDATORY)
|
|
160
|
+
|
|
161
|
+
After creating or editing formulas with openpyxl, values are NOT
|
|
162
|
+
calculated. You MUST run recalculation:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
python scripts/recalc.py output.xlsx
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The script returns JSON:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"status": "success",
|
|
173
|
+
"total_errors": 0,
|
|
174
|
+
"total_formulas": 42,
|
|
175
|
+
"sheets_processed": ["Sheet1", "Summary"]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
If errors are found:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"status": "errors_found",
|
|
184
|
+
"total_errors": 3,
|
|
185
|
+
"total_formulas": 42,
|
|
186
|
+
"error_summary": {
|
|
187
|
+
"#REF!": {"count": 2, "locations": ["Sheet1!B5", "Sheet1!C10"]},
|
|
188
|
+
"#DIV/0!": {"count": 1, "locations": ["Summary!D4"]}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Fix all errors and recalculate again until status is "success".**
|
|
194
|
+
|
|
195
|
+
Common error fixes:
|
|
196
|
+
- `#REF!` — Invalid cell reference; check if rows/columns were deleted
|
|
197
|
+
- `#DIV/0!` — Wrap with `=IFERROR(formula, 0)`
|
|
198
|
+
- `#VALUE!` — Wrong data type; check cell contents
|
|
199
|
+
- `#NAME?` — Typo in function name or missing quotes on strings
|
|
200
|
+
- `#N/A` — VLOOKUP/MATCH found no result; use `=IFERROR()`
|
|
201
|
+
|
|
202
|
+
## Financial Model Standards
|
|
203
|
+
|
|
204
|
+
### Color Coding (Industry Standard)
|
|
205
|
+
|
|
206
|
+
| Color | RGB | Usage |
|
|
207
|
+
|-------|-----|-------|
|
|
208
|
+
| Blue text | `0000FF` | Hardcoded inputs and assumptions |
|
|
209
|
+
| Black text | `000000` | All formulas and calculations |
|
|
210
|
+
| Green text | `008000` | Links to other sheets in same workbook |
|
|
211
|
+
| Red text | `FF0000` | External links to other files |
|
|
212
|
+
| Yellow background | `FFFF00` | Key assumptions needing attention |
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from openpyxl.styles import Font, PatternFill
|
|
216
|
+
|
|
217
|
+
input_font = Font(color='0000FF', name='Arial', size=11)
|
|
218
|
+
formula_font = Font(color='000000', name='Arial', size=11)
|
|
219
|
+
crossref_font = Font(color='008000', name='Arial', size=11)
|
|
220
|
+
attention_fill = PatternFill('solid', fgColor='FFFF00')
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Number Formatting
|
|
224
|
+
|
|
225
|
+
| Data Type | Format Code | Example |
|
|
226
|
+
|-----------|-------------|---------|
|
|
227
|
+
| Currency | `$#,##0;($#,##0);"-"` | $1,500 / ($200) / - |
|
|
228
|
+
| Percentage | `0.0%` | 15.3% |
|
|
229
|
+
| Multiples | `0.0x` | 8.5x |
|
|
230
|
+
| Years | `@` (text) | 2024 (not 2,024) |
|
|
231
|
+
| Large numbers | `$#,##0.0,,"mm"` | $1.5mm |
|
|
232
|
+
| Integers | `#,##0;(#,##0);"-"` | 1,500 / (200) / - |
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
ws['B2'].number_format = '$#,##0;($#,##0);"-"'
|
|
236
|
+
ws['C2'].number_format = '0.0%'
|
|
237
|
+
ws['D2'].number_format = '0.0x'
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Assumptions Placement
|
|
241
|
+
|
|
242
|
+
Place ALL assumptions in dedicated cells with blue text. Never
|
|
243
|
+
hardcode values inside formulas:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
ws['B3'] = 0.05
|
|
247
|
+
ws['B3'].font = input_font
|
|
248
|
+
ws['B3'].number_format = '0.0%'
|
|
249
|
+
ws['B3'].comment = openpyxl.comments.Comment(
|
|
250
|
+
'Revenue growth assumption — Source: Management guidance FY2025',
|
|
251
|
+
'BluMa'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
ws['C5'] = '=C4*(1+$B$3)'
|
|
255
|
+
ws['C5'].font = formula_font
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Documentation for Hardcoded Values
|
|
259
|
+
|
|
260
|
+
Every hardcoded number needs a source comment:
|
|
261
|
+
- `Source: Company 10-K, FY2024, Page 45, Revenue Note`
|
|
262
|
+
- `Source: Bloomberg Terminal, 2025-08-15, AAPL US Equity`
|
|
263
|
+
- `Source: FactSet, Consensus Estimates, 2025-08-20`
|
|
264
|
+
|
|
265
|
+
## Verification Checklist
|
|
266
|
+
|
|
267
|
+
Before delivering any xlsx file:
|
|
268
|
+
|
|
269
|
+
1. Run `scripts/recalc.py` — status must be "success"
|
|
270
|
+
2. Spot-check 3-5 formulas against expected values
|
|
271
|
+
3. Verify column mappings (Excel columns are 1-indexed)
|
|
272
|
+
4. Check row offsets (DataFrame row 5 = Excel row 6 with header)
|
|
273
|
+
5. Confirm no NaN or None values leaked into cells
|
|
274
|
+
6. Validate number formatting on all numeric columns
|
|
275
|
+
7. Ensure all hardcodes have source comments (financial models)
|
|
276
|
+
8. Test edge cases: zero values, empty cells, negative numbers
|
|
277
|
+
|
|
278
|
+
## Library Selection Guide
|
|
279
|
+
|
|
280
|
+
| Task | Use |
|
|
281
|
+
|------|-----|
|
|
282
|
+
| Data analysis, filtering, grouping | pandas |
|
|
283
|
+
| Formulas, formatting, styles | openpyxl |
|
|
284
|
+
| Complex formatting + data analysis | Both (pandas to process, openpyxl to format) |
|
|
285
|
+
| Read calculated values (no edit) | openpyxl with `data_only=True` |
|
|
286
|
+
| Very large files (read-only) | openpyxl with `read_only=True` |
|
|
287
|
+
|
|
288
|
+
## Available References
|
|
289
|
+
|
|
290
|
+
- REFERENCE.md: Advanced financial modeling patterns, chart creation, pivot tables, conditional formatting, data validation, print layout
|
|
291
|
+
|
|
292
|
+
## Available Scripts
|
|
293
|
+
|
|
294
|
+
- recalc.py: Recalculate all formulas via LibreOffice and scan for errors (MANDATORY after formula changes)
|
|
295
|
+
- office/soffice.py: LibreOffice process management for sandboxed environments (internal)
|
|
296
|
+
- office/pack.py: Compress Office XML files into .xlsx (internal)
|
|
297
|
+
- office/unpack.py: Decompress .xlsx into Office XML structure (internal)
|
|
298
|
+
- office/validate.py: Validate Office XML structure against schemas (internal)
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# XLSX — Advanced Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Chart Creation with openpyxl
|
|
4
|
+
|
|
5
|
+
### Bar Chart
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from openpyxl.chart import BarChart, Reference
|
|
9
|
+
|
|
10
|
+
chart = BarChart()
|
|
11
|
+
chart.type = "col"
|
|
12
|
+
chart.title = "Revenue by Quarter"
|
|
13
|
+
chart.y_axis.title = "Revenue ($mm)"
|
|
14
|
+
chart.x_axis.title = "Quarter"
|
|
15
|
+
chart.style = 10
|
|
16
|
+
|
|
17
|
+
data = Reference(ws, min_col=2, min_row=1, max_col=5, max_row=10)
|
|
18
|
+
cats = Reference(ws, min_col=1, min_row=2, max_row=10)
|
|
19
|
+
chart.add_data(data, titles_from_data=True)
|
|
20
|
+
chart.set_categories(cats)
|
|
21
|
+
chart.shape = 4
|
|
22
|
+
|
|
23
|
+
ws.add_chart(chart, "G2")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Line Chart (Time Series)
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from openpyxl.chart import LineChart, Reference
|
|
30
|
+
|
|
31
|
+
chart = LineChart()
|
|
32
|
+
chart.title = "Revenue Trend"
|
|
33
|
+
chart.y_axis.title = "Revenue ($mm)"
|
|
34
|
+
chart.x_axis.title = "Year"
|
|
35
|
+
chart.style = 13
|
|
36
|
+
chart.width = 20
|
|
37
|
+
chart.height = 12
|
|
38
|
+
|
|
39
|
+
data = Reference(ws, min_col=2, min_row=1, max_col=2, max_row=10)
|
|
40
|
+
cats = Reference(ws, min_col=1, min_row=2, max_row=10)
|
|
41
|
+
chart.add_data(data, titles_from_data=True)
|
|
42
|
+
chart.set_categories(cats)
|
|
43
|
+
|
|
44
|
+
series = chart.series[0]
|
|
45
|
+
series.graphicalProperties.line.width = 25000
|
|
46
|
+
|
|
47
|
+
ws.add_chart(chart, "D2")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Pie Chart
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from openpyxl.chart import PieChart, Reference
|
|
54
|
+
|
|
55
|
+
chart = PieChart()
|
|
56
|
+
chart.title = "Market Share"
|
|
57
|
+
chart.style = 26
|
|
58
|
+
|
|
59
|
+
data = Reference(ws, min_col=2, min_row=1, max_row=6)
|
|
60
|
+
cats = Reference(ws, min_col=1, min_row=2, max_row=6)
|
|
61
|
+
chart.add_data(data, titles_from_data=True)
|
|
62
|
+
chart.set_categories(cats)
|
|
63
|
+
|
|
64
|
+
ws.add_chart(chart, "D2")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Conditional Formatting
|
|
68
|
+
|
|
69
|
+
### Color Scale (Heatmap)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from openpyxl.formatting.rule import ColorScaleRule
|
|
73
|
+
|
|
74
|
+
ws.conditional_formatting.add(
|
|
75
|
+
'B2:B100',
|
|
76
|
+
ColorScaleRule(
|
|
77
|
+
start_type='min', start_color='F8696B',
|
|
78
|
+
mid_type='percentile', mid_value=50, mid_color='FFEB84',
|
|
79
|
+
end_type='max', end_color='63BE7B'
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Data Bars
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from openpyxl.formatting.rule import DataBarRule
|
|
88
|
+
|
|
89
|
+
ws.conditional_formatting.add(
|
|
90
|
+
'C2:C100',
|
|
91
|
+
DataBarRule(
|
|
92
|
+
start_type='min', end_type='max',
|
|
93
|
+
color='5B9BD5', showValue=True
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Highlight Cells by Condition
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from openpyxl.formatting.rule import CellIsRule
|
|
102
|
+
from openpyxl.styles import PatternFill
|
|
103
|
+
|
|
104
|
+
red_fill = PatternFill('solid', fgColor='FFC7CE')
|
|
105
|
+
green_fill = PatternFill('solid', fgColor='C6EFCE')
|
|
106
|
+
|
|
107
|
+
ws.conditional_formatting.add(
|
|
108
|
+
'D2:D100',
|
|
109
|
+
CellIsRule(operator='lessThan', formula=['0'], fill=red_fill)
|
|
110
|
+
)
|
|
111
|
+
ws.conditional_formatting.add(
|
|
112
|
+
'D2:D100',
|
|
113
|
+
CellIsRule(operator='greaterThan', formula=['0'], fill=green_fill)
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Data Validation (Dropdowns)
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from openpyxl.worksheet.datavalidation import DataValidation
|
|
121
|
+
|
|
122
|
+
dv = DataValidation(
|
|
123
|
+
type="list",
|
|
124
|
+
formula1='"Option A,Option B,Option C"',
|
|
125
|
+
allow_blank=True
|
|
126
|
+
)
|
|
127
|
+
dv.error = "Invalid selection"
|
|
128
|
+
dv.errorTitle = "Input Error"
|
|
129
|
+
dv.prompt = "Select from the list"
|
|
130
|
+
dv.promptTitle = "Choose Option"
|
|
131
|
+
|
|
132
|
+
ws.add_data_validation(dv)
|
|
133
|
+
dv.add('E2:E100')
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Numeric Range Validation
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
dv_num = DataValidation(
|
|
140
|
+
type="decimal",
|
|
141
|
+
operator="between",
|
|
142
|
+
formula1=0,
|
|
143
|
+
formula2=1
|
|
144
|
+
)
|
|
145
|
+
dv_num.error = "Value must be between 0% and 100%"
|
|
146
|
+
ws.add_data_validation(dv_num)
|
|
147
|
+
dv_num.add('F2:F100')
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Pivot-Style Summary Tables
|
|
151
|
+
|
|
152
|
+
openpyxl does not have native pivot tables. Build them manually:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
import pandas as pd
|
|
156
|
+
from openpyxl import load_workbook
|
|
157
|
+
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
|
|
158
|
+
|
|
159
|
+
df = pd.read_excel('data.xlsx')
|
|
160
|
+
pivot = df.pivot_table(
|
|
161
|
+
index='Category',
|
|
162
|
+
columns='Year',
|
|
163
|
+
values='Revenue',
|
|
164
|
+
aggfunc='sum',
|
|
165
|
+
margins=True,
|
|
166
|
+
margins_name='Total'
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
wb = load_workbook('data.xlsx')
|
|
170
|
+
ws = wb.create_sheet('Pivot')
|
|
171
|
+
|
|
172
|
+
header_font = Font(bold=True, name='Arial', size=11)
|
|
173
|
+
header_fill = PatternFill('solid', fgColor='4472C4')
|
|
174
|
+
header_font_white = Font(bold=True, name='Arial', size=11, color='FFFFFF')
|
|
175
|
+
border = Border(
|
|
176
|
+
bottom=Side(style='thin', color='D9D9D9')
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
ws.cell(row=1, column=1, value='Category').font = header_font_white
|
|
180
|
+
ws['A1'].fill = header_fill
|
|
181
|
+
for j, col_name in enumerate(pivot.columns, 2):
|
|
182
|
+
cell = ws.cell(row=1, column=j, value=str(col_name))
|
|
183
|
+
cell.font = header_font_white
|
|
184
|
+
cell.fill = header_fill
|
|
185
|
+
cell.alignment = Alignment(horizontal='center')
|
|
186
|
+
|
|
187
|
+
for i, (idx, row_data) in enumerate(pivot.iterrows(), 2):
|
|
188
|
+
ws.cell(row=i, column=1, value=idx).font = Font(
|
|
189
|
+
bold=(idx == 'Total'), name='Arial', size=11
|
|
190
|
+
)
|
|
191
|
+
for j, val in enumerate(row_data, 2):
|
|
192
|
+
cell = ws.cell(row=i, column=j, value=val)
|
|
193
|
+
cell.number_format = '$#,##0;($#,##0);"-"'
|
|
194
|
+
cell.border = border
|
|
195
|
+
if idx == 'Total':
|
|
196
|
+
cell.font = Font(bold=True, name='Arial', size=11)
|
|
197
|
+
|
|
198
|
+
wb.save('data.xlsx')
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Print Layout Configuration
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
ws.page_setup.orientation = 'landscape'
|
|
205
|
+
ws.page_setup.paperSize = ws.PAPERSIZE_A4
|
|
206
|
+
ws.page_setup.fitToWidth = 1
|
|
207
|
+
ws.page_setup.fitToHeight = 0
|
|
208
|
+
|
|
209
|
+
ws.sheet_properties.pageSetUpPr.fitToPage = True
|
|
210
|
+
|
|
211
|
+
ws.oddHeader.center.text = "Company Name — Confidential"
|
|
212
|
+
ws.oddFooter.left.text = "Page &P of &N"
|
|
213
|
+
ws.oddFooter.right.text = "&D"
|
|
214
|
+
|
|
215
|
+
ws.print_title_rows = '1:2'
|
|
216
|
+
ws.print_area = 'A1:H50'
|
|
217
|
+
|
|
218
|
+
ws.page_margins = openpyxl.worksheet.page.PageMargins(
|
|
219
|
+
left=0.5, right=0.5, top=0.75, bottom=0.75,
|
|
220
|
+
header=0.3, footer=0.3
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Freeze Panes and Auto-Filter
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
ws.freeze_panes = 'B2'
|
|
228
|
+
|
|
229
|
+
ws.auto_filter.ref = f'A1:{get_column_letter(ws.max_column)}{ws.max_row}'
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Named Ranges
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from openpyxl.workbook.defined_name import DefinedName
|
|
236
|
+
|
|
237
|
+
ref = f"'{ws.title}'!$B$3"
|
|
238
|
+
defn = DefinedName('GrowthRate', attr_text=ref)
|
|
239
|
+
wb.defined_names.add(defn)
|
|
240
|
+
|
|
241
|
+
ws['C5'] = '=C4*(1+GrowthRate)'
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Merging Cells and Grouped Headers
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
ws.merge_cells('A1:D1')
|
|
248
|
+
ws['A1'] = 'Quarterly Revenue Summary'
|
|
249
|
+
ws['A1'].font = Font(bold=True, size=14, name='Arial')
|
|
250
|
+
ws['A1'].alignment = Alignment(horizontal='center')
|
|
251
|
+
|
|
252
|
+
ws.merge_cells('B2:C2')
|
|
253
|
+
ws['B2'] = 'Q1-Q2'
|
|
254
|
+
ws.merge_cells('D2:E2')
|
|
255
|
+
ws['D2'] = 'Q3-Q4'
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Working with Large Datasets
|
|
259
|
+
|
|
260
|
+
### Write-Only Mode (memory efficient)
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
wb = Workbook(write_only=True)
|
|
264
|
+
ws = wb.create_sheet()
|
|
265
|
+
|
|
266
|
+
for row_data in generate_rows():
|
|
267
|
+
ws.append(row_data)
|
|
268
|
+
|
|
269
|
+
wb.save('large_output.xlsx')
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Read-Only Mode
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
wb = load_workbook('large_file.xlsx', read_only=True)
|
|
276
|
+
ws = wb.active
|
|
277
|
+
|
|
278
|
+
for row in ws.iter_rows(min_row=2, values_only=True):
|
|
279
|
+
process(row)
|
|
280
|
+
|
|
281
|
+
wb.close()
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Financial Model Templates
|
|
285
|
+
|
|
286
|
+
### DCF Valuation Layout
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
Sheet: Assumptions
|
|
290
|
+
B3: Revenue Growth Rate (blue, input)
|
|
291
|
+
B4: EBITDA Margin (blue, input)
|
|
292
|
+
B5: Tax Rate (blue, input)
|
|
293
|
+
B6: WACC (blue, input)
|
|
294
|
+
B7: Terminal Growth (blue, input)
|
|
295
|
+
|
|
296
|
+
Sheet: Projections
|
|
297
|
+
Row 1: Headers (Year 1, Year 2, ... Year 5)
|
|
298
|
+
Row 3: Revenue = =Prior*(1+Assumptions!$B$3)
|
|
299
|
+
Row 4: EBITDA = =B3*Assumptions!$B$4
|
|
300
|
+
Row 5: D&A (blue, input)
|
|
301
|
+
Row 6: EBIT = =B4-B5
|
|
302
|
+
Row 7: Tax = =B6*Assumptions!$B$5
|
|
303
|
+
Row 8: NOPAT = =B6-B7
|
|
304
|
+
Row 9: CapEx (blue, input)
|
|
305
|
+
Row 10: Change in WC (blue, input)
|
|
306
|
+
Row 11: FCF = =B8+B5-B9-B10
|
|
307
|
+
|
|
308
|
+
Sheet: Valuation
|
|
309
|
+
B3: PV of FCFs = =NPV(Assumptions!$B$6, Projections!B11:F11)
|
|
310
|
+
B4: Terminal Value = =Projections!F11*(1+Assumptions!$B$7)/(Assumptions!$B$6-Assumptions!$B$7)
|
|
311
|
+
B5: PV of Terminal = =B4/(1+Assumptions!$B$6)^5
|
|
312
|
+
B6: Enterprise Value = =B3+B5
|
|
313
|
+
B7: Less: Net Debt (blue, input)
|
|
314
|
+
B8: Equity Value = =B6-B7
|
|
315
|
+
B9: Shares Outstanding (blue, input)
|
|
316
|
+
B10: Implied Price = =B8/B9
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Sensitivity Table
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
base_row = 15
|
|
323
|
+
scenarios = [-0.02, -0.01, 0, 0.01, 0.02]
|
|
324
|
+
|
|
325
|
+
for i, delta in enumerate(scenarios):
|
|
326
|
+
col = i + 2
|
|
327
|
+
ws.cell(row=base_row, column=col, value=base_wacc + delta)
|
|
328
|
+
ws.cell(row=base_row, column=col).number_format = '0.0%'
|
|
329
|
+
|
|
330
|
+
for j, tg_delta in enumerate(scenarios):
|
|
331
|
+
row = base_row + 1 + j
|
|
332
|
+
ws.cell(row=row, column=1, value=base_tg + tg_delta)
|
|
333
|
+
ws.cell(row=row, column=1).number_format = '0.0%'
|
|
334
|
+
ws.cell(row=row, column=col).value = (
|
|
335
|
+
f'=Projections!F11*(1+A{row})/({get_column_letter(col)}{base_row}-A{row})'
|
|
336
|
+
)
|
|
337
|
+
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pack an unpacked Office Open XML directory back into an .xlsx/.docx/.pptx file.
|
|
3
|
+
|
|
4
|
+
Office files are ZIP archives containing XML. This module compresses a
|
|
5
|
+
directory structure back into a valid Office file, respecting the required
|
|
6
|
+
entry order ([Content_Types].xml first).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import zipfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CONTENT_TYPES = '[Content_Types].xml'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def pack(source_dir: str, output_path: str) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Compress a directory into an Office Open XML file.
|
|
19
|
+
|
|
20
|
+
The [Content_Types].xml file is written first as required by the
|
|
21
|
+
Office Open XML specification.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
source_dir: Path to the unpacked directory.
|
|
25
|
+
output_path: Destination .xlsx/.docx/.pptx file path.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Absolute path to the created file.
|
|
29
|
+
"""
|
|
30
|
+
abs_source = os.path.abspath(source_dir)
|
|
31
|
+
abs_output = os.path.abspath(output_path)
|
|
32
|
+
|
|
33
|
+
if not os.path.isdir(abs_source):
|
|
34
|
+
raise FileNotFoundError(f'Source directory not found: {abs_source}')
|
|
35
|
+
|
|
36
|
+
entries = []
|
|
37
|
+
ct_path = None
|
|
38
|
+
|
|
39
|
+
for root, _, files in os.walk(abs_source):
|
|
40
|
+
for fname in files:
|
|
41
|
+
full = os.path.join(root, fname)
|
|
42
|
+
arcname = os.path.relpath(full, abs_source)
|
|
43
|
+
|
|
44
|
+
if arcname == CONTENT_TYPES:
|
|
45
|
+
ct_path = full
|
|
46
|
+
else:
|
|
47
|
+
entries.append((full, arcname))
|
|
48
|
+
|
|
49
|
+
entries.sort(key=lambda x: x[1])
|
|
50
|
+
|
|
51
|
+
with zipfile.ZipFile(abs_output, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
52
|
+
if ct_path:
|
|
53
|
+
zf.write(ct_path, CONTENT_TYPES)
|
|
54
|
+
|
|
55
|
+
for full_path, arcname in entries:
|
|
56
|
+
zf.write(full_path, arcname)
|
|
57
|
+
|
|
58
|
+
return abs_output
|