Initial commit: skills library
- 70 skills with code and documentation - Add .gitignore (ignore __pycache__, output/, temp/, venv/) - Clean up test intermediates and caches
This commit is contained in:
+288
@@ -0,0 +1,288 @@
|
||||
---
|
||||
name: xlsx
|
||||
description: "Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas"
|
||||
---
|
||||
|
||||
# Requirements for Outputs
|
||||
|
||||
## All Excel files
|
||||
|
||||
### Zero Formula Errors
|
||||
- Every Excel model MUST be delivered with ZERO formula errors (#REF!, #DIV/0!, #VALUE!, #N/A, #NAME?)
|
||||
|
||||
### Preserve Existing Templates (when updating templates)
|
||||
- Study and EXACTLY match existing format, style, and conventions when modifying files
|
||||
- Never impose standardized formatting on files with established patterns
|
||||
- Existing template conventions ALWAYS override these guidelines
|
||||
|
||||
## Financial models
|
||||
|
||||
### Color Coding Standards
|
||||
Unless otherwise stated by the user or existing template
|
||||
|
||||
#### Industry-Standard Color Conventions
|
||||
- **Blue text (RGB: 0,0,255)**: Hardcoded inputs, and numbers users will change for scenarios
|
||||
- **Black text (RGB: 0,0,0)**: ALL formulas and calculations
|
||||
- **Green text (RGB: 0,128,0)**: Links pulling from other worksheets within same workbook
|
||||
- **Red text (RGB: 255,0,0)**: External links to other files
|
||||
- **Yellow background (RGB: 255,255,0)**: Key assumptions needing attention or cells that need to be updated
|
||||
|
||||
### Number Formatting Standards
|
||||
|
||||
#### Required Format Rules
|
||||
- **Years**: Format as text strings (e.g., "2024" not "2,024")
|
||||
- **Currency**: Use $#,##0 format; ALWAYS specify units in headers ("Revenue ($mm)")
|
||||
- **Zeros**: Use number formatting to make all zeros "-", including percentages (e.g., "$#,##0;($#,##0);-")
|
||||
- **Percentages**: Default to 0.0% format (one decimal)
|
||||
- **Multiples**: Format as 0.0x for valuation multiples (EV/EBITDA, P/E)
|
||||
- **Negative numbers**: Use parentheses (123) not minus -123
|
||||
|
||||
### Formula Construction Rules
|
||||
|
||||
#### Assumptions Placement
|
||||
- Place ALL assumptions (growth rates, margins, multiples, etc.) in separate assumption cells
|
||||
- Use cell references instead of hardcoded values in formulas
|
||||
- Example: Use =B5*(1+$B$6) instead of =B5*1.05
|
||||
|
||||
#### Formula Error Prevention
|
||||
- Verify all cell references are correct
|
||||
- Check for off-by-one errors in ranges
|
||||
- Ensure consistent formulas across all projection periods
|
||||
- Test with edge cases (zero values, negative numbers)
|
||||
- Verify no unintended circular references
|
||||
|
||||
#### Documentation Requirements for Hardcodes
|
||||
- Comment or in cells beside (if end of table). Format: "Source: [System/Document], [Date], [Specific Reference], [URL if applicable]"
|
||||
- Examples:
|
||||
- "Source: Company 10-K, FY2024, Page 45, Revenue Note, [SEC EDGAR URL]"
|
||||
- "Source: Company 10-Q, Q2 2025, Exhibit 99.1, [SEC EDGAR URL]"
|
||||
- "Source: Bloomberg Terminal, 8/15/2025, AAPL US Equity"
|
||||
- "Source: FactSet, 8/20/2025, Consensus Estimates Screen"
|
||||
|
||||
# XLSX creation, editing, and analysis
|
||||
|
||||
## Overview
|
||||
|
||||
A user may ask you to create, edit, or analyze the contents of an .xlsx file. You have different tools and workflows available for different tasks.
|
||||
|
||||
## Important Requirements
|
||||
|
||||
**LibreOffice Required for Formula Recalculation**: You can assume LibreOffice is installed for recalculating formula values using the `recalc.py` script. The script automatically configures LibreOffice on first run
|
||||
|
||||
## Reading and analyzing data
|
||||
|
||||
### Data analysis with pandas
|
||||
For data analysis, visualization, and basic operations, use **pandas** which provides powerful data manipulation capabilities:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Read Excel
|
||||
df = pd.read_excel('file.xlsx') # Default: first sheet
|
||||
all_sheets = pd.read_excel('file.xlsx', sheet_name=None) # All sheets as dict
|
||||
|
||||
# Analyze
|
||||
df.head() # Preview data
|
||||
df.info() # Column info
|
||||
df.describe() # Statistics
|
||||
|
||||
# Write Excel
|
||||
df.to_excel('output.xlsx', index=False)
|
||||
```
|
||||
|
||||
## Excel File Workflows
|
||||
|
||||
## CRITICAL: Use Formulas, Not Hardcoded Values
|
||||
|
||||
**Always use Excel formulas instead of calculating values in Python and hardcoding them.** This ensures the spreadsheet remains dynamic and updateable.
|
||||
|
||||
### ❌ WRONG - Hardcoding Calculated Values
|
||||
```python
|
||||
# Bad: Calculating in Python and hardcoding result
|
||||
total = df['Sales'].sum()
|
||||
sheet['B10'] = total # Hardcodes 5000
|
||||
|
||||
# Bad: Computing growth rate in Python
|
||||
growth = (df.iloc[-1]['Revenue'] - df.iloc[0]['Revenue']) / df.iloc[0]['Revenue']
|
||||
sheet['C5'] = growth # Hardcodes 0.15
|
||||
|
||||
# Bad: Python calculation for average
|
||||
avg = sum(values) / len(values)
|
||||
sheet['D20'] = avg # Hardcodes 42.5
|
||||
```
|
||||
|
||||
### ✅ CORRECT - Using Excel Formulas
|
||||
```python
|
||||
# Good: Let Excel calculate the sum
|
||||
sheet['B10'] = '=SUM(B2:B9)'
|
||||
|
||||
# Good: Growth rate as Excel formula
|
||||
sheet['C5'] = '=(C4-C2)/C2'
|
||||
|
||||
# Good: Average using Excel function
|
||||
sheet['D20'] = '=AVERAGE(D2:D19)'
|
||||
```
|
||||
|
||||
This applies to ALL calculations - totals, percentages, ratios, differences, etc. The spreadsheet should be able to recalculate when source data changes.
|
||||
|
||||
## Common Workflow
|
||||
1. **Choose tool**: pandas for data, openpyxl for formulas/formatting
|
||||
2. **Create/Load**: Create new workbook or load existing file
|
||||
3. **Modify**: Add/edit data, formulas, and formatting
|
||||
4. **Save**: Write to file
|
||||
5. **Recalculate formulas (MANDATORY IF USING FORMULAS)**: Use the recalc.py script
|
||||
```bash
|
||||
python recalc.py output.xlsx
|
||||
```
|
||||
6. **Verify and fix any errors**:
|
||||
- The script returns JSON with error details
|
||||
- If `status` is `errors_found`, check `error_summary` for specific error types and locations
|
||||
- Fix the identified errors and recalculate again
|
||||
- Common errors to fix:
|
||||
- `#REF!`: Invalid cell references
|
||||
- `#DIV/0!`: Division by zero
|
||||
- `#VALUE!`: Wrong data type in formula
|
||||
- `#NAME?`: Unrecognized formula name
|
||||
|
||||
### Creating new Excel files
|
||||
|
||||
```python
|
||||
# Using openpyxl for formulas and formatting
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
|
||||
wb = Workbook()
|
||||
sheet = wb.active
|
||||
|
||||
# Add data
|
||||
sheet['A1'] = 'Hello'
|
||||
sheet['B1'] = 'World'
|
||||
sheet.append(['Row', 'of', 'data'])
|
||||
|
||||
# Add formula
|
||||
sheet['B2'] = '=SUM(A1:A10)'
|
||||
|
||||
# Formatting
|
||||
sheet['A1'].font = Font(bold=True, color='FF0000')
|
||||
sheet['A1'].fill = PatternFill('solid', start_color='FFFF00')
|
||||
sheet['A1'].alignment = Alignment(horizontal='center')
|
||||
|
||||
# Column width
|
||||
sheet.column_dimensions['A'].width = 20
|
||||
|
||||
wb.save('output.xlsx')
|
||||
```
|
||||
|
||||
### Editing existing Excel files
|
||||
|
||||
```python
|
||||
# Using openpyxl to preserve formulas and formatting
|
||||
from openpyxl import load_workbook
|
||||
|
||||
# Load existing file
|
||||
wb = load_workbook('existing.xlsx')
|
||||
sheet = wb.active # or wb['SheetName'] for specific sheet
|
||||
|
||||
# Working with multiple sheets
|
||||
for sheet_name in wb.sheetnames:
|
||||
sheet = wb[sheet_name]
|
||||
print(f"Sheet: {sheet_name}")
|
||||
|
||||
# Modify cells
|
||||
sheet['A1'] = 'New Value'
|
||||
sheet.insert_rows(2) # Insert row at position 2
|
||||
sheet.delete_cols(3) # Delete column 3
|
||||
|
||||
# Add new sheet
|
||||
new_sheet = wb.create_sheet('NewSheet')
|
||||
new_sheet['A1'] = 'Data'
|
||||
|
||||
wb.save('modified.xlsx')
|
||||
```
|
||||
|
||||
## Recalculating formulas
|
||||
|
||||
Excel files created or modified by openpyxl contain formulas as strings but not calculated values. Use the provided `recalc.py` script to recalculate formulas:
|
||||
|
||||
```bash
|
||||
python recalc.py <excel_file> [timeout_seconds]
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
python recalc.py output.xlsx 30
|
||||
```
|
||||
|
||||
The script:
|
||||
- Automatically sets up LibreOffice macro on first run
|
||||
- Recalculates all formulas in all sheets
|
||||
- Scans ALL cells for Excel errors (#REF!, #DIV/0!, etc.)
|
||||
- Returns JSON with detailed error locations and counts
|
||||
- Works on both Linux and macOS
|
||||
|
||||
## Formula Verification Checklist
|
||||
|
||||
Quick checks to ensure formulas work correctly:
|
||||
|
||||
### Essential Verification
|
||||
- [ ] **Test 2-3 sample references**: Verify they pull correct values before building full model
|
||||
- [ ] **Column mapping**: Confirm Excel columns match (e.g., column 64 = BL, not BK)
|
||||
- [ ] **Row offset**: Remember Excel rows are 1-indexed (DataFrame row 5 = Excel row 6)
|
||||
|
||||
### Common Pitfalls
|
||||
- [ ] **NaN handling**: Check for null values with `pd.notna()`
|
||||
- [ ] **Far-right columns**: FY data often in columns 50+
|
||||
- [ ] **Multiple matches**: Search all occurrences, not just first
|
||||
- [ ] **Division by zero**: Check denominators before using `/` in formulas (#DIV/0!)
|
||||
- [ ] **Wrong references**: Verify all cell references point to intended cells (#REF!)
|
||||
- [ ] **Cross-sheet references**: Use correct format (Sheet1!A1) for linking sheets
|
||||
|
||||
### Formula Testing Strategy
|
||||
- [ ] **Start small**: Test formulas on 2-3 cells before applying broadly
|
||||
- [ ] **Verify dependencies**: Check all cells referenced in formulas exist
|
||||
- [ ] **Test edge cases**: Include zero, negative, and very large values
|
||||
|
||||
### Interpreting recalc.py Output
|
||||
The script returns JSON with error details:
|
||||
```json
|
||||
{
|
||||
"status": "success", // or "errors_found"
|
||||
"total_errors": 0, // Total error count
|
||||
"total_formulas": 42, // Number of formulas in file
|
||||
"error_summary": { // Only present if errors found
|
||||
"#REF!": {
|
||||
"count": 2,
|
||||
"locations": ["Sheet1!B5", "Sheet1!C10"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Library Selection
|
||||
- **pandas**: Best for data analysis, bulk operations, and simple data export
|
||||
- **openpyxl**: Best for complex formatting, formulas, and Excel-specific features
|
||||
|
||||
### Working with openpyxl
|
||||
- Cell indices are 1-based (row=1, column=1 refers to cell A1)
|
||||
- Use `data_only=True` to read calculated values: `load_workbook('file.xlsx', data_only=True)`
|
||||
- **Warning**: If opened with `data_only=True` and saved, formulas are replaced with values and permanently lost
|
||||
- For large files: Use `read_only=True` for reading or `write_only=True` for writing
|
||||
- Formulas are preserved but not evaluated - use recalc.py to update values
|
||||
|
||||
### Working with pandas
|
||||
- Specify data types to avoid inference issues: `pd.read_excel('file.xlsx', dtype={'id': str})`
|
||||
- For large files, read specific columns: `pd.read_excel('file.xlsx', usecols=['A', 'C', 'E'])`
|
||||
- Handle dates properly: `pd.read_excel('file.xlsx', parse_dates=['date_column'])`
|
||||
|
||||
## Code Style Guidelines
|
||||
**IMPORTANT**: When generating Python code for Excel operations:
|
||||
- Write minimal, concise Python code without unnecessary comments
|
||||
- Avoid verbose variable names and redundant operations
|
||||
- Avoid unnecessary print statements
|
||||
|
||||
**For Excel files themselves**:
|
||||
- Add comments to cells with complex formulas or important assumptions
|
||||
- Document data sources for hardcoded values
|
||||
- Include notes for key calculations and model sections
|
||||
@@ -0,0 +1,374 @@
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
|
||||
wb = Workbook()
|
||||
|
||||
blue = Font(name="Microsoft YaHei", size=11, color="0000FF")
|
||||
black = Font(name="Microsoft YaHei", size=11, color="000000")
|
||||
bold = Font(name="Microsoft YaHei", size=11, color="000000", bold=True)
|
||||
bold_blue = Font(name="Microsoft YaHei", size=11, color="0000FF", bold=True)
|
||||
hdr = Font(name="Microsoft YaHei", size=12, color="FFFFFF", bold=True)
|
||||
lblue = PatternFill("solid", fgColor="DCE6F1")
|
||||
lgreen = PatternFill("solid", fgColor="E2EFDA")
|
||||
lgray = PatternFill("solid", fgColor="F2F2F2")
|
||||
highlight = PatternFill("solid", fgColor="FFF2CC")
|
||||
center = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
left = Alignment(horizontal="left", vertical="center", wrap_text=True)
|
||||
bdr = Border(
|
||||
left=Side(style="thin"),
|
||||
right=Side(style="thin"),
|
||||
top=Side(style="thin"),
|
||||
bottom=Side(style="thin"),
|
||||
)
|
||||
cur = '¥#,##0;("¥"#,##0);-'
|
||||
pct = "0.0%"
|
||||
hr = '¥#,##0"元/h"'
|
||||
num = "#,##0"
|
||||
|
||||
|
||||
def sc(ws, r, c, v, font=black, fill=None, fmt=None, align=center, merge_to=None):
|
||||
cell = ws.cell(row=r, column=c, value=v)
|
||||
cell.font = font
|
||||
if fill:
|
||||
cell.fill = fill
|
||||
if fmt:
|
||||
cell.number_format = fmt
|
||||
cell.alignment = align
|
||||
cell.border = bdr
|
||||
if merge_to:
|
||||
ws.merge_cells(start_row=r, start_column=c, end_row=r, end_column=merge_to)
|
||||
|
||||
|
||||
def hdr_row(ws, r, vals, bg="2F5496"):
|
||||
f = PatternFill("solid", fgColor=bg)
|
||||
for i, v in enumerate(vals, 1):
|
||||
sc(ws, r, i, v, font=hdr, fill=f)
|
||||
|
||||
|
||||
# ════════════ Sheet 1 ════════════
|
||||
ws1 = wb.active
|
||||
ws1.title = "第一期"
|
||||
ws1.sheet_properties.tabColor = "4472C4"
|
||||
for c in range(1, 4):
|
||||
ws1.column_dimensions[chr(64 + c)].width = 24
|
||||
|
||||
sc(
|
||||
ws1,
|
||||
1,
|
||||
1,
|
||||
"青年钢琴集体课 · 教师薪酬测算 — 第一期(公益入门)",
|
||||
font=bold,
|
||||
align=left,
|
||||
merge_to=3,
|
||||
)
|
||||
sc(ws1, 2, 1, "A 类教师(研发教师)", font=bold_blue, align=left, merge_to=3)
|
||||
hdr_row(ws1, 4, ["项目", "数值", "说明"])
|
||||
|
||||
inputs1 = [
|
||||
("▎输入区(蓝色可改)", None, None),
|
||||
("学员学费(元/期)", 600, "10+1 次课"),
|
||||
("满班人数", 13, ""),
|
||||
("平台抽成比例(含渠道)", 0.126, "12%+0.6%"),
|
||||
("教师基础分成比例", 0.50, "A 类锁定 2 年"),
|
||||
("研发奖金(元)", 1000, "仅第 1 期"),
|
||||
("续课奖金(元)", 0, "仅统计基线"),
|
||||
("总课时数", 11, "含 1 节免费体验"),
|
||||
("单课时长(分钟)", 90, ""),
|
||||
("预估覆盖成本(元/期)", 3000, "房租/折旧/管理等"),
|
||||
]
|
||||
for i, (label, val, note) in enumerate(inputs1):
|
||||
r = 5 + i
|
||||
if val is None:
|
||||
sc(ws1, r, 1, label, font=bold, fill=lblue, align=left, merge_to=3)
|
||||
else:
|
||||
sc(ws1, r, 1, label, font=blue, fill=lblue, align=left)
|
||||
sc(ws1, r, 2, val, font=blue, fill=lblue)
|
||||
sc(ws1, r, 3, note, font=blue, fill=lblue, align=left)
|
||||
if any(k in label for k in ["学费", "奖金", "成本"]):
|
||||
ws1.cell(row=r, column=2).number_format = cur
|
||||
elif "比例" in label:
|
||||
ws1.cell(row=r, column=2).number_format = pct
|
||||
|
||||
calcs1 = [
|
||||
("▎计算区(自动)", None, None),
|
||||
("总学费收入", "=B6*B7", "学费 × 人数"),
|
||||
("平台抽成金额", "=B17*B8", "总学费 × 抽成"),
|
||||
("机构实收", "=B17-B18", "总学费 - 抽成"),
|
||||
("", "", ""),
|
||||
("教师基础分成", "=B19*B9", "实收 × 分成比例"),
|
||||
("研发奖金", "=B10", ""),
|
||||
("续课奖金", "=B11", ""),
|
||||
("教师一期总收入", "=B21+B22+B23", ""),
|
||||
("", "", ""),
|
||||
("机构剩余", "=B19-B24", "实收 - 教师总收入"),
|
||||
("机构净利润", "=B26-B14", "剩余 - 覆盖成本"),
|
||||
("", "", ""),
|
||||
("教师单次出勤收益", "=B24/B12", "总收入 ÷ 课时数"),
|
||||
("折合时薪", "=B29/(B13/60)", "单次收益 ÷ 小时"),
|
||||
]
|
||||
for i, (label, val, note) in enumerate(calcs1):
|
||||
r = 16 + i
|
||||
if val is None:
|
||||
sc(ws1, r, 1, label, font=bold, fill=lgreen, align=left, merge_to=3)
|
||||
else:
|
||||
sc(ws1, r, 1, label, font=black, fill=lgreen, align=left)
|
||||
sc(ws1, r, 2, val, font=black, fill=lgreen)
|
||||
sc(ws1, r, 3, note, font=black, fill=lgreen, align=left)
|
||||
if any(
|
||||
k in label
|
||||
for k in [
|
||||
"收入",
|
||||
"分成",
|
||||
"奖金",
|
||||
"剩余",
|
||||
"利润",
|
||||
"出勤",
|
||||
"学费",
|
||||
"抽成",
|
||||
"实收",
|
||||
]
|
||||
):
|
||||
ws1.cell(row=r, column=2).number_format = cur
|
||||
elif "时薪" in label:
|
||||
ws1.cell(row=r, column=2).number_format = hr
|
||||
|
||||
for rr in [24, 27, 29, 30]:
|
||||
ws1.cell(row=rr, column=2).font = bold
|
||||
ws1.cell(row=rr, column=2).fill = highlight
|
||||
|
||||
# ════════════ Sheet 2 ════════════
|
||||
ws2 = wb.create_sheet("第二期")
|
||||
ws2.sheet_properties.tabColor = "548235"
|
||||
for c in range(1, 7):
|
||||
ws2.column_dimensions[chr(64 + c)].width = 22
|
||||
|
||||
sc(
|
||||
ws2,
|
||||
1,
|
||||
1,
|
||||
"青年钢琴集体课 · 教师薪酬测算 — 第二期起(双班并行)",
|
||||
font=bold,
|
||||
align=left,
|
||||
merge_to=6,
|
||||
)
|
||||
|
||||
# ── 公共参数 ──
|
||||
hdr_row(ws2, 3, ["项目", "数值", "说明", "项目", "数值", "说明"])
|
||||
common = [
|
||||
("▎公共参数", "", "", "▎进阶班参数(自营)", "", ""),
|
||||
("入门班课时数", 10, "", "进阶班课时数", 10, ""),
|
||||
("入门班单课时长 (min)", 60, "", "进阶班单课时长 (min)", 60, ""),
|
||||
("预估覆盖成本 (元/期·双班)", 6000, "", "", "", ""),
|
||||
]
|
||||
for i, row_data in enumerate(common):
|
||||
r = 4 + i
|
||||
for j, v in enumerate(row_data):
|
||||
is_hdr = j in (0, 3)
|
||||
sc(
|
||||
ws2,
|
||||
r,
|
||||
j + 1,
|
||||
v,
|
||||
font=bold if is_hdr else blue,
|
||||
fill=lblue,
|
||||
align=left if is_hdr else center,
|
||||
)
|
||||
if i == 3:
|
||||
ws2.cell(row=r, column=2).number_format = cur
|
||||
|
||||
# ── A 类 ──
|
||||
sc(ws2, 9, 1, "A 类教师(研发教师)", font=bold_blue, align=left, merge_to=6)
|
||||
hdr_row(ws2, 10, ["项目", "入门班", "进阶班", "双班合计", "", ""])
|
||||
|
||||
# 输入区:按次定价 → 按期自动计算
|
||||
a_in = [
|
||||
("学员学费/单价 (元/次)", 60, 118, cur),
|
||||
("学员学费/单价 (元/期)", "=B11*B5", "=C11*E5", cur),
|
||||
("满班人数", 18, 18, num),
|
||||
("平台抽成比例", 0.126, 0.0, pct),
|
||||
("基础分成比例", 0.40, 0.40, pct),
|
||||
("浮动分成比例 (=续课奖金)", 0.10, 0.10, pct),
|
||||
("研发奖金 (元)", 500, 0, cur),
|
||||
]
|
||||
for i, (lb, b1, b2, fmt) in enumerate(a_in):
|
||||
r = 11 + i
|
||||
sc(ws2, r, 1, lb, font=blue, fill=lblue, align=left)
|
||||
sc(ws2, r, 2, b1, font=black if str(b1).startswith("=") else blue, fill=lblue)
|
||||
sc(ws2, r, 3, b2, font=black if str(b2).startswith("=") else blue, fill=lblue)
|
||||
ws2.cell(row=r, column=2).number_format = fmt
|
||||
ws2.cell(row=r, column=3).number_format = fmt
|
||||
|
||||
# A 计算区
|
||||
sc(ws2, 19, 1, "▎计算区", font=bold, fill=lgreen, align=left, merge_to=6)
|
||||
|
||||
a_rows = [
|
||||
("总学费收入", "=B12*B13", "=C12*C13", cur),
|
||||
("平台抽成金额", "=B20*B14", "=C20*C14", cur),
|
||||
("机构实收", "=B20-B21", "=C20-C21", cur),
|
||||
("", "", "", None),
|
||||
("教师基础分成", "=B22*B15", "=C22*C15", cur),
|
||||
("教师浮动分成/续课奖金 (满额)", "=B22*B16", "=C22*C16", cur),
|
||||
("教师单班总收入", "=B24+B25+B17", "=C24+C25+C17", cur),
|
||||
("", "", "", None),
|
||||
("▎双班合计(A 类)", "", "", None),
|
||||
("教师双班总收入", "=B26+C26", "", cur),
|
||||
("单次出勤收益", "=B29/B5", "", cur),
|
||||
("折合时薪", "=B30/((B6+E6)/60)", "", hr),
|
||||
("", "", "", None),
|
||||
("机构双班总实收", "=B22+C22", "", cur),
|
||||
("机构净利润 (A 类带双班)", "=B33-B29-B7", "", cur),
|
||||
]
|
||||
for i, (lb, b1, b2, fmt) in enumerate(a_rows):
|
||||
r = 20 + i
|
||||
is_sec = "▎" in lb
|
||||
sc(ws2, r, 1, lb, font=bold if is_sec else black, fill=lgreen, align=left)
|
||||
sc(ws2, r, 2, b1, font=black if str(b1).startswith("=") else blue, fill=lgreen)
|
||||
sc(ws2, r, 3, b2, font=black if str(b2).startswith("=") else blue, fill=lgreen)
|
||||
if fmt:
|
||||
ws2.cell(row=r, column=2).number_format = fmt
|
||||
if b2:
|
||||
ws2.cell(row=r, column=3).number_format = fmt
|
||||
|
||||
for rr in [26, 29, 30, 31, 34]:
|
||||
ws2.cell(row=rr, column=2).font = bold
|
||||
ws2.cell(row=rr, column=2).fill = highlight
|
||||
|
||||
# ── B 类 ──
|
||||
sc(ws2, 36, 1, "B 类教师(普通接手)", font=bold_blue, align=left, merge_to=6)
|
||||
hdr_row(ws2, 37, ["项目", "入门班", "进阶班", "双班合计", "", ""])
|
||||
|
||||
b_in = [
|
||||
("学员学费/单价 (元/次)", 60, 118, cur),
|
||||
("学员学费/单价 (元/期)", "=B38*B5", "=C38*E5", cur),
|
||||
("满班人数", 18, 18, num),
|
||||
("平台抽成比例", 0.126, 0.0, pct),
|
||||
("基础分成比例", 0.30, 0.30, pct),
|
||||
("浮动分成比例 (=续课奖金)", 0.10, 0.10, pct),
|
||||
("研发奖金 (元)", 0, 0, cur),
|
||||
]
|
||||
for i, (lb, b1, b2, fmt) in enumerate(b_in):
|
||||
r = 38 + i
|
||||
sc(ws2, r, 1, lb, font=blue, fill=lblue, align=left)
|
||||
sc(ws2, r, 2, b1, font=black if str(b1).startswith("=") else blue, fill=lblue)
|
||||
sc(ws2, r, 3, b2, font=black if str(b2).startswith("=") else blue, fill=lblue)
|
||||
ws2.cell(row=r, column=2).number_format = fmt
|
||||
ws2.cell(row=r, column=3).number_format = fmt
|
||||
|
||||
sc(ws2, 46, 1, "▎计算区", font=bold, fill=lgreen, align=left, merge_to=6)
|
||||
|
||||
b_rows = [
|
||||
("总学费收入", "=B39*B40", "=C39*C40", cur),
|
||||
("平台抽成金额", "=B47*B41", "=C47*C41", cur),
|
||||
("机构实收", "=B47-B48", "=C47-C48", cur),
|
||||
("", "", "", None),
|
||||
("教师基础分成", "=B49*B42", "=C49*C42", cur),
|
||||
("教师浮动分成/续课奖金 (满额)", "=B49*B43", "=C49*C43", cur),
|
||||
("教师单班总收入", "=B51+B52+B44", "=C51+C52+C44", cur),
|
||||
("", "", "", None),
|
||||
("▎双班合计(B 类)", "", "", None),
|
||||
("教师双班总收入", "=B53+C53", "", cur),
|
||||
("单次出勤收益", "=B56/B5", "", cur),
|
||||
("折合时薪", "=B57/((B6+E6)/60)", "", hr),
|
||||
("", "", "", None),
|
||||
("机构双班总实收", "=B49+C49", "", cur),
|
||||
("机构净利润 (B 类带双班)", "=B60-B56-B7", "", cur),
|
||||
]
|
||||
for i, (lb, b1, b2, fmt) in enumerate(b_rows):
|
||||
r = 47 + i
|
||||
is_sec = "▎" in lb
|
||||
sc(ws2, r, 1, lb, font=bold if is_sec else black, fill=lgreen, align=left)
|
||||
sc(ws2, r, 2, b1, font=black if str(b1).startswith("=") else blue, fill=lgreen)
|
||||
sc(ws2, r, 3, b2, font=black if str(b2).startswith("=") else blue, fill=lgreen)
|
||||
if fmt:
|
||||
ws2.cell(row=r, column=2).number_format = fmt
|
||||
if b2:
|
||||
ws2.cell(row=r, column=3).number_format = fmt
|
||||
|
||||
for rr in [53, 56, 57, 58, 61]:
|
||||
ws2.cell(row=rr, column=2).font = bold
|
||||
ws2.cell(row=rr, column=2).fill = highlight
|
||||
|
||||
# ── 续课阶梯 ──
|
||||
# 续课奖金 = 浮动分成满额 × 阶梯系数
|
||||
sc(
|
||||
ws2,
|
||||
63,
|
||||
1,
|
||||
"▎续课奖金阶梯表(= 浮动分成满额 × 系数)",
|
||||
font=bold,
|
||||
fill=lgray,
|
||||
align=left,
|
||||
merge_to=6,
|
||||
)
|
||||
hdr_row(ws2, 64, ["续课率", "系数", "A 类奖金 (公式)", "B 类奖金 (公式)", "", ""])
|
||||
|
||||
# A 类浮动满额 = B25+C25, B 类 = B52+C52
|
||||
bonus_rows = [
|
||||
("≥ 80%", 1.0, "=ROUND((B25+C25)*B65,0)", "=ROUND((B52+C52)*B65,0)"),
|
||||
("60% ~ 80%", 0.6, "=ROUND((B25+C25)*B66,0)", "=ROUND((B52+C52)*B66,0)"),
|
||||
("40% ~ 60%", 0.3, "=ROUND((B25+C25)*B67,0)", "=ROUND((B52+C52)*B67,0)"),
|
||||
("< 40%", 0.0, "=ROUND((B25+C25)*B68,0)", "=ROUND((B52+C52)*B68,0)"),
|
||||
]
|
||||
for i, (rate, coef, a_f, b_f) in enumerate(bonus_rows):
|
||||
r = 65 + i
|
||||
sc(ws2, r, 1, rate, font=black, fill=lgreen)
|
||||
sc(ws2, r, 2, coef, font=black, fill=lgreen, fmt=pct)
|
||||
sc(ws2, r, 3, a_f, font=black, fill=lgreen, fmt=cur)
|
||||
sc(ws2, r, 4, b_f, font=black, fill=lgreen, fmt=cur)
|
||||
|
||||
# ── 敏感性分析 ──
|
||||
sc(
|
||||
ws2,
|
||||
70,
|
||||
1,
|
||||
"▎敏感性分析:续课率对 A 类教师收入的影响",
|
||||
font=bold,
|
||||
fill=lgray,
|
||||
align=left,
|
||||
merge_to=6,
|
||||
)
|
||||
hdr_row(
|
||||
ws2,
|
||||
71,
|
||||
["续课率", "续课奖金", "教师一期总收入", "单次出勤", "折合时薪", "机构净利润"],
|
||||
)
|
||||
|
||||
sens = [
|
||||
(
|
||||
"80%",
|
||||
"=C65",
|
||||
"=B29-(B25+C25)+C65",
|
||||
"=C72/B5",
|
||||
"=D72/((B6+E6)/60)",
|
||||
"=B33-C72-B7",
|
||||
),
|
||||
(
|
||||
"60%",
|
||||
"=C66",
|
||||
"=B29-(B25+C25)+C66",
|
||||
"=C73/B5",
|
||||
"=D73/((B6+E6)/60)",
|
||||
"=B33-C73-B7",
|
||||
),
|
||||
(
|
||||
"40%",
|
||||
"=C67",
|
||||
"=B29-(B25+C25)+C67",
|
||||
"=C74/B5",
|
||||
"=D74/((B6+E6)/60)",
|
||||
"=B33-C74-B7",
|
||||
),
|
||||
("0%", "=C68", "=B29-(B25+C25)+C68", "=C75/B5", "=D75/((B6+E6)/60)", "=B33-C75-B7"),
|
||||
]
|
||||
for i, row_data in enumerate(sens):
|
||||
r = 72 + i
|
||||
for j, v in enumerate(row_data):
|
||||
is_f = str(v).startswith("=")
|
||||
sc(ws2, r, j + 1, v, font=black if is_f else blue, fill=lgreen)
|
||||
fmt_map = {1: cur, 2: cur, 3: cur, 4: hr, 5: cur}
|
||||
if j in fmt_map:
|
||||
ws2.cell(row=r, column=j + 1).number_format = fmt_map[j]
|
||||
|
||||
out = "D:/F/NewI/opencode/daily-workspace/projects/青年钢琴集体课/04-薪酬与协议/教师薪酬测算模型.xlsx"
|
||||
wb.save(out)
|
||||
print("Saved:", out)
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Excel Formula Recalculation Script
|
||||
Recalculates all formulas in an Excel file using LibreOffice
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
||||
def setup_libreoffice_macro():
|
||||
"""Setup LibreOffice macro for recalculation if not already configured"""
|
||||
if platform.system() == 'Darwin':
|
||||
macro_dir = os.path.expanduser('~/Library/Application Support/LibreOffice/4/user/basic/Standard')
|
||||
else:
|
||||
macro_dir = os.path.expanduser('~/.config/libreoffice/4/user/basic/Standard')
|
||||
|
||||
macro_file = os.path.join(macro_dir, 'Module1.xba')
|
||||
|
||||
if os.path.exists(macro_file):
|
||||
with open(macro_file, 'r') as f:
|
||||
if 'RecalculateAndSave' in f.read():
|
||||
return True
|
||||
|
||||
if not os.path.exists(macro_dir):
|
||||
subprocess.run(['soffice', '--headless', '--terminate_after_init'],
|
||||
capture_output=True, timeout=10)
|
||||
os.makedirs(macro_dir, exist_ok=True)
|
||||
|
||||
macro_content = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
|
||||
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
|
||||
Sub RecalculateAndSave()
|
||||
ThisComponent.calculateAll()
|
||||
ThisComponent.store()
|
||||
ThisComponent.close(True)
|
||||
End Sub
|
||||
</script:module>'''
|
||||
|
||||
try:
|
||||
with open(macro_file, 'w') as f:
|
||||
f.write(macro_content)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def recalc(filename, timeout=30):
|
||||
"""
|
||||
Recalculate formulas in Excel file and report any errors
|
||||
|
||||
Args:
|
||||
filename: Path to Excel file
|
||||
timeout: Maximum time to wait for recalculation (seconds)
|
||||
|
||||
Returns:
|
||||
dict with error locations and counts
|
||||
"""
|
||||
if not Path(filename).exists():
|
||||
return {'error': f'File {filename} does not exist'}
|
||||
|
||||
abs_path = str(Path(filename).absolute())
|
||||
|
||||
if not setup_libreoffice_macro():
|
||||
return {'error': 'Failed to setup LibreOffice macro'}
|
||||
|
||||
cmd = [
|
||||
'soffice', '--headless', '--norestore',
|
||||
'vnd.sun.star.script:Standard.Module1.RecalculateAndSave?language=Basic&location=application',
|
||||
abs_path
|
||||
]
|
||||
|
||||
# Handle timeout command differences between Linux and macOS
|
||||
if platform.system() != 'Windows':
|
||||
timeout_cmd = 'timeout' if platform.system() == 'Linux' else None
|
||||
if platform.system() == 'Darwin':
|
||||
# Check if gtimeout is available on macOS
|
||||
try:
|
||||
subprocess.run(['gtimeout', '--version'], capture_output=True, timeout=1, check=False)
|
||||
timeout_cmd = 'gtimeout'
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
if timeout_cmd:
|
||||
cmd = [timeout_cmd, str(timeout)] + cmd
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0 and result.returncode != 124: # 124 is timeout exit code
|
||||
error_msg = result.stderr or 'Unknown error during recalculation'
|
||||
if 'Module1' in error_msg or 'RecalculateAndSave' not in error_msg:
|
||||
return {'error': 'LibreOffice macro not configured properly'}
|
||||
else:
|
||||
return {'error': error_msg}
|
||||
|
||||
# Check for Excel errors in the recalculated file - scan ALL cells
|
||||
try:
|
||||
wb = load_workbook(filename, data_only=True)
|
||||
|
||||
excel_errors = ['#VALUE!', '#DIV/0!', '#REF!', '#NAME?', '#NULL!', '#NUM!', '#N/A']
|
||||
error_details = {err: [] for err in excel_errors}
|
||||
total_errors = 0
|
||||
|
||||
for sheet_name in wb.sheetnames:
|
||||
ws = wb[sheet_name]
|
||||
# Check ALL rows and columns - no limits
|
||||
for row in ws.iter_rows():
|
||||
for cell in row:
|
||||
if cell.value is not None and isinstance(cell.value, str):
|
||||
for err in excel_errors:
|
||||
if err in cell.value:
|
||||
location = f"{sheet_name}!{cell.coordinate}"
|
||||
error_details[err].append(location)
|
||||
total_errors += 1
|
||||
break
|
||||
|
||||
wb.close()
|
||||
|
||||
# Build result summary
|
||||
result = {
|
||||
'status': 'success' if total_errors == 0 else 'errors_found',
|
||||
'total_errors': total_errors,
|
||||
'error_summary': {}
|
||||
}
|
||||
|
||||
# Add non-empty error categories
|
||||
for err_type, locations in error_details.items():
|
||||
if locations:
|
||||
result['error_summary'][err_type] = {
|
||||
'count': len(locations),
|
||||
'locations': locations[:20] # Show up to 20 locations
|
||||
}
|
||||
|
||||
# Add formula count for context - also check ALL cells
|
||||
wb_formulas = load_workbook(filename, data_only=False)
|
||||
formula_count = 0
|
||||
for sheet_name in wb_formulas.sheetnames:
|
||||
ws = wb_formulas[sheet_name]
|
||||
for row in ws.iter_rows():
|
||||
for cell in row:
|
||||
if cell.value and isinstance(cell.value, str) and cell.value.startswith('='):
|
||||
formula_count += 1
|
||||
wb_formulas.close()
|
||||
|
||||
result['total_formulas'] = formula_count
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python recalc.py <excel_file> [timeout_seconds]")
|
||||
print("\nRecalculates all formulas in an Excel file using LibreOffice")
|
||||
print("\nReturns JSON with error details:")
|
||||
print(" - status: 'success' or 'errors_found'")
|
||||
print(" - total_errors: Total number of Excel errors found")
|
||||
print(" - total_formulas: Number of formulas in the file")
|
||||
print(" - error_summary: Breakdown by error type with locations")
|
||||
print(" - #VALUE!, #DIV/0!, #REF!, #NAME?, #NULL!, #NUM!, #N/A")
|
||||
sys.exit(1)
|
||||
|
||||
filename = sys.argv[1]
|
||||
timeout = int(sys.argv[2]) if len(sys.argv) > 2 else 30
|
||||
|
||||
result = recalc(filename, timeout)
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,5 @@
|
||||
# XLSX Skill - Excel处理依赖
|
||||
openpyxl>=3.1.0
|
||||
|
||||
# 系统工具(需要单独安装)
|
||||
# LibreOffice: 用于公式重算
|
||||
Reference in New Issue
Block a user