feat: add week report generator from JSON input

- Add week_report_gen_from_json skill
- Support hierarchical project structure and cell merging
- Add example scripts and update README
This commit is contained in:
MarioYang 2026-01-23 17:12:01 +08:00
parent cf7992fcf5
commit 23ad22bebc
4 changed files with 1015 additions and 1 deletions

View File

@ -0,0 +1,102 @@
# Week Report Generator from JSON
這個技能基於 `week_work_package_log` 的 JSON 輸出生成項目週報,與原始的 `week_report_gen` 功能相同但輸入來源不同。
## 主要差異
### 輸入格式
- **原始 week_report_gen**: Excel 成本報告檔案 (cost-report-*.xls)
- **新 week_report_gen_from_json**: JSON 格式的工時數據
### 數據來源
- **原始**: 手動匯出的 Excel 檔案
- **新**: 直接從 OpenProject API 獲取的結構化數據
### 欄位邏輯
按照新需求實現的邏輯:
#### B欄 (專案名稱)
顯示最上級專案名稱(`type: "project"` 的節點)
#### C欄 (子項目名稱)
- 如果底層任務屬於子專案 → 顯示最上級的 `work_package`
- 如果沒有中間層級 → 顯示 `"-"`
#### D欄 (進度)
跟隨 C 欄合併,因為進度是針對整個子項目的狀態
#### E欄 (本周主要進展)
顯示最底層工作包名稱(有 `time_entries` 的節點),如果有 comment 也一併顯示
## 使用方法
### 1. 指定日期範圍(自動獲取數據)
```bash
python generate_report.py "2026-01-20" "2026-01-22" "output.xlsx"
```
### 2. 使用已有的 JSON 檔案
```bash
python generate_report.py "data.json" "output.xlsx"
```
### 3. 使用預設值(本週數據)
```bash
python generate_report.py
```
## 功能特色
- ✓ 自動從 OpenProject API 獲取數據
- ✓ 支援使用者名稱映射 (UserManager)
- ✓ 階層專案結構識別
- ✓ 工時聚合和評論合併
- ✓ 智能儲存格合併邏輯
- **B 欄**: 相同專案名稱合併
- **C 欄**: 相同子項目名稱合併("-" 不合併)
- **D 欄**: 跟隨 C 欄合併(進度針對子項目)
- ✓ 使用相同模板格式
- ✓ 完整的樣式和格式化
## 排序邏輯
數據按以下順序排序:
1. **專案名稱** (升序)
2. **子項目名稱** (升序)
3. **工時** (降序)
這確保相同專案和子項目的條目會聚集在一起,便於合併儲存格。
## 合併邏輯
### 專案層級合併 (B 欄)
- 相同專案名稱的多行會合併 B 欄儲存格
- 合併儲存格垂直置中對齊
### 子項目層級合併 (C & D 欄)
- 相同子項目名稱的多行會同時合併 C 欄和 D 欄儲存格
- **特殊處理**: 子項目名稱為 `"-"` 的條目不會合併(因為 "-" 表示沒有子項目,每個都是獨立的)
- 合併儲存格垂直置中對齊
## 依賴技能
- `week_work_package_log`: 獲取 JSON 數據
- `list_user`: 使用者名稱映射 (可選)
- `week_report_gen`: 模板檔案共用
## 輸出格式
生成的 Excel 檔案格式與原始 `week_report_gen` 完全相同:
- 公司標題和日期範圍
- 標準化的欄位結構
- 專案分組和合併儲存格
- 一致的顏色和邊框樣式
- 專業的視覺呈現效果
## 範例輸出
生成的週報會包含:
- **專案分組**: 每個專案的條目會聚集在一起
- **子項目合併**: 相同子項目的進度欄位統一顯示
- **清晰的層次**: 從專案 → 子項目 → 具體任務的清晰結構
- **工時統計**: 每個任務的詳細工時記錄

View File

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
Week Report Generator from JSON 使用範例腳本
展示基於 JSON 數據生成週報的各種使用場景
"""
import sys
import os
from datetime import datetime, timedelta
# Add the skill to Python path
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, script_dir)
from generate_report import generate_weekly_report_from_json
def example_this_week():
"""範例1: 生成本週週報(自動獲取數據)"""
today = datetime.now()
days_since_monday = today.weekday()
this_monday = today - timedelta(days=days_since_monday)
this_friday = this_monday + timedelta(days=4)
start_date = this_monday.strftime("%Y-%m-%d")
end_date = this_friday.strftime("%Y-%m-%d")
print(f"📅 範例1: 本週週報生成 ({start_date} ~ {end_date})")
print("=" * 70)
try:
output_file = f"temp/週報-本週-{datetime.now().strftime('%Y%m%d')}.xlsx"
output_path, report_df = generate_weekly_report_from_json(
json_input=(start_date, end_date),
output_file=output_file
)
print(f"\n✅ 週報生成成功!")
print(f"📁 輸出檔案: {output_path}")
print(f"📊 包含 {len(report_df)} 個工時記錄")
print(f"🏢 涵蓋 {report_df['專案名稱'].nunique()} 個專案")
except Exception as e:
print(f"❌ 錯誤: {e}")
def example_specific_dates():
"""範例2: 生成指定日期範圍的週報"""
start_date = "2026-01-20"
end_date = "2026-01-22"
print(f"📅 範例2: 指定日期週報 ({start_date} ~ {end_date})")
print("=" * 70)
try:
output_file = f"temp/週報-指定日期-{end_date.replace('-', '')}.xlsx"
output_path, report_df = generate_weekly_report_from_json(
json_input=(start_date, end_date),
output_file=output_file,
team_name="智能控制組"
)
print(f"\n✅ 週報生成成功!")
print(f"📁 輸出檔案: {output_path}")
# 顯示專案摘要
print(f"\n📋 專案摘要:")
for project in report_df['專案名稱'].unique():
project_data = report_df[report_df['專案名稱'] == project]
total_hours = project_data['工時'].sum()
print(f"{project}: {total_hours} 小時 ({len(project_data)} 個任務)")
except Exception as e:
print(f"❌ 錯誤: {e}")
def example_with_json_file():
"""範例3: 從現有 JSON 檔案生成週報"""
print("📅 範例3: 從 JSON 檔案生成週報")
print("=" * 70)
# 先生成 JSON 數據(模擬已有的數據文件)
from week_work_package_log.work_package_logger import WeekWorkPackageLogger
try:
# 獲取 JSON 數據
logger = WeekWorkPackageLogger()
json_data = logger.fetch_and_display_time_entries("2026-01-20", "2026-01-22", 'json')
if json_data:
# 保存為檔案
import json
json_file = "temp/work_data.json"
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(json_data, f, ensure_ascii=False, indent=2)
print(f"💾 JSON 數據已保存到: {json_file}")
# 從 JSON 檔案生成週報
output_file = "temp/週報-from-json-file.xlsx"
output_path, report_df = generate_weekly_report_from_json(
json_input=json_file,
output_file=output_file
)
print(f"\n✅ 從 JSON 檔案生成週報成功!")
print(f"📁 輸出檔案: {output_path}")
except Exception as e:
print(f"❌ 錯誤: {e}")
def show_features():
"""展示主要功能特色"""
print("🚀 Week Report Generator from JSON 功能特色")
print("=" * 70)
print("✓ 直接從 OpenProject API 獲取數據")
print("✓ 智能儲存格合併:")
print(" - B 欄:相同專案名稱自動合併")
print(" - C 欄:相同子項目名稱自動合併")
print(" - D 欄:跟隨子項目進度合併")
print("✓ 階層專案結構識別")
print("✓ 使用者名稱自動映射")
print("✓ 專業週報模板格式")
print("✓ 支援多種輸入方式日期範圍、JSON檔案、預設值")
print("✓ 自動排序和分組")
print("✓ 工時聚合和評論合併")
def main():
"""主函數 - 展示所有範例"""
print("🎯 Week Report Generator from JSON 使用範例")
print("=" * 70)
# 確保 temp 目錄存在
os.makedirs("temp", exist_ok=True)
# 展示功能特色
show_features()
print("\n")
# 執行範例
example_this_week()
print("\n")
example_specific_dates()
print("\n")
example_with_json_file()
print("\n")
print("🎉 所有範例執行完畢!")
print("📁 請檢查 temp/ 目錄中生成的週報檔案")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,721 @@
"""
Weekly Report Generator from JSON
This script generates weekly project reports from week_work_package_log JSON output.
"""
import pandas as pd
import openpyxl
import json
from datetime import datetime
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from copy import copy
import os
import sys
# Add list_user skill to path for user mapping
script_dir = os.path.dirname(os.path.abspath(__file__))
skills_dir = os.path.dirname(script_dir) # .../skills
list_user_path = os.path.join(skills_dir, 'list_user')
if list_user_path not in sys.path:
sys.path.insert(0, list_user_path)
try:
from user_manager import UserManager
USER_MANAGER_AVAILABLE = True
except ImportError as e:
USER_MANAGER_AVAILABLE = False
# Add week_work_package_log skill to path for JSON data generation
work_package_log_path = os.path.join(skills_dir, 'week_work_package_log')
if work_package_log_path not in sys.path:
sys.path.insert(0, work_package_log_path)
try:
from work_package_logger import WeekWorkPackageLogger
WORK_PACKAGE_LOG_AVAILABLE = True
except ImportError as e:
WORK_PACKAGE_LOG_AVAILABLE = False
# Style constants for consistent coloring
HEADER_FILL_COLOR = 'FF2F75B5' # Blue header background
DATA_FILL_COLOR = 'FFE7E6E6' # Light gray data background
WHITE_BORDER = Border(
left=Side(style='thin', color='FFFFFFFF'),
right=Side(style='thin', color='FFFFFFFF'),
top=Side(style='thin', color='FFFFFFFF'),
bottom=Side(style='thin', color='FFFFFFFF')
)
def copy_cell_style(source_cell, target_cell):
"""
Copy all styling from source cell to target cell.
"""
if source_cell.has_style:
target_cell.font = copy(source_cell.font)
target_cell.border = copy(source_cell.border)
target_cell.fill = copy(source_cell.fill)
target_cell.number_format = copy(source_cell.number_format)
target_cell.protection = copy(source_cell.protection)
target_cell.alignment = copy(source_cell.alignment)
def copy_sheet_with_format(source_sheet, target_sheet, max_rows=10):
"""
Copy formatting from source sheet template to target sheet.
Only copies the first few rows (headers) to preserve template styling.
"""
# Copy row heights
for row_idx in range(1, max_rows + 1):
if row_idx in source_sheet.row_dimensions:
target_sheet.row_dimensions[row_idx].height = source_sheet.row_dimensions[row_idx].height
# Copy column widths
for col_letter in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']:
if col_letter in source_sheet.column_dimensions:
target_sheet.column_dimensions[col_letter].width = source_sheet.column_dimensions[col_letter].width
# Copy cell formatting for header rows (first max_rows)
for row_idx in range(1, max_rows + 1):
for col_idx in range(1, 12): # Columns A-K
source_cell = source_sheet.cell(row=row_idx, column=col_idx)
target_cell = target_sheet.cell(row=row_idx, column=col_idx)
# Copy style
copy_cell_style(source_cell, target_cell)
# Copy merged cells info (we'll handle merging separately)
# Copy merged cells from template (only header rows)
for merged_range in source_sheet.merged_cells.ranges:
if merged_range.min_row <= max_rows and merged_range.max_row <= max_rows:
try:
target_sheet.merge_cells(str(merged_range))
except:
pass # Skip if already merged or invalid
def style_header_row(ws, row_idx, template_cell=None, col_start=2, col_end=10):
"""
Apply header styling (blue fill, bold, centered, white grid) to a row.
If template_cell is provided, its style is copied before applying borders.
"""
for col_idx in range(col_start, col_end + 1):
cell = ws.cell(row=row_idx, column=col_idx)
if template_cell and template_cell.has_style:
copy_cell_style(template_cell, cell)
else:
cell.fill = PatternFill(start_color=HEADER_FILL_COLOR, end_color=HEADER_FILL_COLOR, fill_type='solid')
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
cell.border = WHITE_BORDER
def style_data_row(ws, row_idx, fill_color=DATA_FILL_COLOR, col_start=2, col_end=10):
"""
Apply data row styling (fill, wrap text, white grid) to a row.
"""
data_fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type='solid')
data_align = Alignment(vertical='top', wrap_text=True)
for col_idx in range(col_start, col_end + 1):
cell = ws.cell(row=row_idx, column=col_idx)
cell.fill = data_fill
cell.alignment = data_align
cell.border = WHITE_BORDER
def read_json_data(json_input):
"""
Read JSON data from file or string and parse the structure.
Args:
json_input: Either file path to JSON file or JSON string
Returns:
dict: Parsed JSON data
"""
try:
if os.path.exists(json_input):
# Input is a file path
with open(json_input, 'r', encoding='utf-8') as f:
data = json.load(f)
else:
# Input is a JSON string
data = json.loads(json_input)
# Validate JSON structure
if 'projects' not in data or 'summary' not in data:
raise ValueError("Invalid JSON format: missing 'projects' or 'summary' key")
return data
except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"Error reading JSON data: {e}")
raise
except Exception as e:
print(f"Error processing JSON data: {e}")
raise
def find_leaf_work_packages(node, path=[]):
"""
Recursively find all leaf work packages (those with time_entries).
Args:
node: Current node in the tree
path: Current path from root to this node
Returns:
list: List of (leaf_node, path_to_leaf) tuples
"""
current_path = path + [node]
leaf_packages = []
# If this node has time_entries, it's a leaf
if node.get('time_entries', []):
leaf_packages.append((node, current_path))
# Recursively search children
for child in node.get('children', []):
leaf_packages.extend(find_leaf_work_packages(child, current_path))
return leaf_packages
def extract_report_data_from_json(json_data, user_manager=None):
"""
Extract report data from JSON structure following the new logic.
Args:
json_data: Parsed JSON data from week_work_package_log
user_manager: Optional UserManager for name mapping
Returns:
tuple: (report_data_list, start_date, end_date)
"""
report_data = []
# Extract date range from summary
date_range = json_data['summary']['date_range']
start_date = datetime.strptime(date_range.split(' to ')[0], '%Y-%m-%d')
end_date = datetime.strptime(date_range.split(' to ')[1], '%Y-%m-%d')
# Process each project
for project in json_data['projects']:
# Find all leaf work packages in this project
leaf_packages = find_leaf_work_packages(project)
for leaf_node, path_to_leaf in leaf_packages:
# Process each time entry in the leaf node
for time_entry in leaf_node['time_entries']:
user_name = time_entry['user']
# Map nickname to formal name if user manager is available
if user_manager:
formal_name = user_manager.get_formal_name(user_name, fallback='keep_original')
participant = formal_name
else:
participant = user_name
# Determine project name (B column) - always the root project
project_name = path_to_leaf[0]['name'] # First element is always the project
# Determine sub-project name (C column)
sub_project_name = "-"
if len(path_to_leaf) > 2: # project -> work_package -> ... -> leaf
# Find the highest level work_package (first work_package in path)
for node in path_to_leaf[1:]: # Skip project node
if node['type'] == 'work_package':
sub_project_name = node['name']
break
# Task name (E column) - leaf work package name
task_name = leaf_node['name']
# Format progress text with comments
progress_parts = [task_name]
if time_entry.get('comment') and time_entry['comment'].strip():
progress_parts.append(time_entry['comment'].strip())
progress_text = '\n'.join(progress_parts)
report_data.append({
'專案名稱': project_name,
'子項目名稱': sub_project_name,
'進度': None, # User should fill this in
'本周主要進展': progress_text,
'參與人員': participant,
'工時': time_entry['hours'],
'交付物': None,
'代碼上傳': None,
'下周計畫': None
})
return report_data, start_date, end_date
def format_for_weekly_report(json_data, user_manager=None):
"""
Transform JSON data into weekly report format with aggregation.
Groups by project, sub-project, user, and task, merging comments.
Args:
json_data: Parsed JSON data from week_work_package_log
user_manager: Optional UserManager instance for name mapping
Returns:
pd.DataFrame: Formatted report data
"""
# Extract individual entries
report_entries, start_date, end_date = extract_report_data_from_json(json_data, user_manager)
# Group entries for aggregation
grouped_entries = {}
for entry in report_entries:
# Create grouping key: project + sub-project + user + task
group_key = (
entry['專案名稱'],
entry['子項目名稱'],
entry['參與人員'],
entry['本周主要進展'].split('\n')[0] # Task name (first line)
)
if group_key not in grouped_entries:
grouped_entries[group_key] = {
'專案名稱': entry['專案名稱'],
'子項目名稱': entry['子項目名稱'],
'進度': None,
'本周主要進展': [entry['本周主要進展'].split('\n')[0]], # Task name
'參與人員': entry['參與人員'],
'工時': 0,
'交付物': None,
'代碼上傳': None,
'下周計畫': None,
'comments': []
}
# Aggregate hours
grouped_entries[group_key]['工時'] += entry['工時']
# Collect comments (skip task name)
entry_lines = entry['本周主要進展'].split('\n')
if len(entry_lines) > 1: # Has comments beyond task name
for comment in entry_lines[1:]:
comment = comment.strip()
if comment and comment not in grouped_entries[group_key]['comments']:
grouped_entries[group_key]['comments'].append(comment)
# Format final entries
final_report_data = []
for group_key, group_data in grouped_entries.items():
# Format progress text: task name + comments
progress_parts = group_data['本周主要進展'] # Task name
if group_data['comments']:
progress_parts.extend(group_data['comments'])
progress_text = '\n'.join(progress_parts)
final_report_data.append({
'專案名稱': group_data['專案名稱'],
'子項目名稱': group_data['子項目名稱'],
'進度': group_data['進度'],
'本周主要進展': progress_text,
'參與人員': group_data['參與人員'],
'工時': group_data['工時'],
'交付物': group_data['交付物'],
'代碼上傳': group_data['代碼上傳'],
'下周計畫': group_data['下周計畫']
})
# Sort by project name first, then by sub-project name, then by hours (descending)
final_report_data.sort(key=lambda x: (x['專案名稱'], x['子項目名稱'], -x['工時']))
return pd.DataFrame(final_report_data), start_date, end_date
def fetch_json_data(start_date, end_date):
"""
Fetch JSON data using week_work_package_log skill.
Args:
start_date: Start date in YYYY-MM-DD format
end_date: End date in YYYY-MM-DD format
Returns:
dict: JSON data from work package logger
"""
if not WORK_PACKAGE_LOG_AVAILABLE:
raise ImportError("week_work_package_log skill not available")
try:
logger = WeekWorkPackageLogger()
json_data = logger.fetch_and_display_time_entries(start_date, end_date, 'json')
if not json_data:
raise ValueError(f"No data returned for date range {start_date} to {end_date}")
return json_data
except Exception as e:
print(f"Error fetching work package data: {e}")
raise
def generate_weekly_report_from_json(json_input, output_file, template_file=None, team_name="智能控制組", use_user_mapping=True):
"""
Main function to generate weekly report from JSON data.
Args:
json_input: JSON file path, JSON string, or tuple (start_date, end_date) to fetch data
output_file: Path to output weekly report Excel file
template_file: Optional path to template file
team_name: Team name for sheet title (default: "智能控制組")
use_user_mapping: Whether to apply user name mapping (default: True)
Returns:
Tuple of (output_file_path, report_dataframe)
"""
# Initialize user manager for name mapping
user_manager = None
if use_user_mapping and USER_MANAGER_AVAILABLE:
try:
user_manager = UserManager()
print("✓ User name mapping enabled")
except Exception as e:
print(f"⚠ Warning: Could not load user mapping: {e}")
print(" Continuing without user name mapping...")
elif use_user_mapping and not USER_MANAGER_AVAILABLE:
print("⚠ Warning: list_user skill not available, skipping user name mapping")
# Get JSON data
if isinstance(json_input, tuple) and len(json_input) == 2:
# Fetch data using dates
start_date, end_date = json_input
print(f"Fetching work package data from {start_date} to {end_date}...")
json_data = fetch_json_data(start_date, end_date)
else:
# Read from file or string
print(f"Reading JSON data from: {json_input}")
json_data = read_json_data(json_input)
print(f"Loaded JSON data with {len(json_data['projects'])} projects")
# Process data
report_df, start_date, end_date = format_for_weekly_report(json_data, user_manager=user_manager)
print(f"Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
print(f"Formatted {len(report_df)} work entries for report")
# Load template or create new workbook
if template_file and os.path.exists(template_file):
print(f"Loading template from: {template_file}")
wb = openpyxl.load_workbook(template_file)
# Get the template sheet
template_sheet = None
for sheet_name_candidate in ['周报示例', wb.sheetnames[0]]:
if sheet_name_candidate in wb.sheetnames:
template_sheet = wb[sheet_name_candidate]
break
# Create new sheet for this week
sheet_name = f"{end_date.strftime('%Y%m%d')}-{team_name}"
ws = wb.create_sheet(sheet_name, 0) # Insert at beginning
# Copy formatting from template if available
if template_sheet:
print(f"Copying formatting from template sheet: {template_sheet.title}")
copy_sheet_with_format(template_sheet, ws, max_rows=4)
# Unmerge cells beyond row 4 to allow data writing
merged_ranges_to_remove = []
for merged_range in ws.merged_cells.ranges:
if merged_range.min_row > 4:
merged_ranges_to_remove.append(merged_range)
for merged_range in merged_ranges_to_remove:
ws.unmerge_cells(str(merged_range))
else:
print("Creating new workbook (no template provided)")
wb = openpyxl.Workbook()
ws = wb.active
sheet_name = f"{end_date.strftime('%Y%m%d')}-{team_name}"
ws.title = sheet_name
template_sheet = None
# Write header
ws['B1'] = '弘訊科技股份有限公司'
ws['B2'] = f"2026年度週報({start_date.strftime('%m月%d')}-{end_date.strftime('%m月%d')}"
ws['B3'] = '建議用可視化素材(如有)輔助報告:設計圖、產品架構圖、產品實物照片等等相關材料'
# Write column headers (row 4)
headers = ['專案名稱', '子項目名稱', '進度', '本周主要進展 (請條列說明)',
'參與人員', '工时(个人投入研发工时', '交付物', '代碼(如有)是否上傳', '下周計畫']
for col_idx, header in enumerate(headers, start=2): # Start from column B
ws.cell(row=4, column=col_idx, value=header)
# Ensure header row is uniformly blue
template_header_cell = template_sheet.cell(row=4, column=2) if template_sheet else None
style_header_row(ws, row_idx=4, template_cell=template_header_cell)
# Write data starting from row 5
current_row = 5
current_project = None
current_sub_project = None
project_start_row = None
sub_project_start_row = None
for idx, row_data in report_df.iterrows():
project_name = row_data['專案名稱']
sub_project_name = row_data['子項目名稱']
# Check if we're starting a new project
if project_name != current_project:
# Merge cells for the previous project if it had multiple rows
if current_project is not None and project_start_row is not None and current_row > project_start_row + 1:
try:
ws.merge_cells(f'B{project_start_row}:B{current_row - 1}')
# Center the merged cell content
merged_cell = ws.cell(row=project_start_row, column=2)
merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
except:
pass # Skip if merge fails
# Merge cells for the previous sub-project if it had multiple rows and is not "-"
if (current_sub_project is not None and current_sub_project != "-" and
sub_project_start_row is not None and current_row > sub_project_start_row + 1):
try:
# Merge C column (sub-project name)
ws.merge_cells(f'C{sub_project_start_row}:C{current_row - 1}')
merged_cell = ws.cell(row=sub_project_start_row, column=3)
merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
# Merge D column (progress) as well
ws.merge_cells(f'D{sub_project_start_row}:D{current_row - 1}')
merged_cell_d = ws.cell(row=sub_project_start_row, column=4)
merged_cell_d.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
except:
pass # Skip if merge fails
# Start tracking new project
current_project = project_name
project_start_row = current_row
current_sub_project = sub_project_name
sub_project_start_row = current_row
# Check if we're starting a new sub-project (within same project)
elif sub_project_name != current_sub_project:
# Merge cells for the previous sub-project if it had multiple rows and is not "-"
if (current_sub_project is not None and current_sub_project != "-" and
sub_project_start_row is not None and current_row > sub_project_start_row + 1):
try:
# Merge C column (sub-project name)
ws.merge_cells(f'C{sub_project_start_row}:C{current_row - 1}')
merged_cell = ws.cell(row=sub_project_start_row, column=3)
merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
# Merge D column (progress) as well
ws.merge_cells(f'D{sub_project_start_row}:D{current_row - 1}')
merged_cell_d = ws.cell(row=sub_project_start_row, column=4)
merged_cell_d.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
except:
pass # Skip if merge fails
# Start tracking new sub-project
current_sub_project = sub_project_name
sub_project_start_row = current_row
# Write data for current row
ws.cell(row=current_row, column=2, value=project_name)
ws.cell(row=current_row, column=3, value=row_data['子項目名稱'])
ws.cell(row=current_row, column=4, value=row_data['進度'])
ws.cell(row=current_row, column=5, value=row_data['本周主要進展'])
ws.cell(row=current_row, column=6, value=row_data['參與人員'])
ws.cell(row=current_row, column=7, value=row_data['工時'])
ws.cell(row=current_row, column=8, value=row_data['交付物'])
ws.cell(row=current_row, column=9, value=row_data['代碼上傳'])
ws.cell(row=current_row, column=10, value=row_data['下周計畫'])
# Apply consistent gray fill and white borders to data row
style_data_row(ws, row_idx=current_row)
current_row += 1
# Add separator row (except after last entry)
if idx < len(report_df) - 1:
# Check if next entry is different project
next_project = report_df.iloc[idx + 1]['專案名稱'] if idx + 1 < len(report_df) else None
if next_project != project_name:
# Merge cells for current project before adding separator
if project_start_row is not None and current_row > project_start_row:
try:
ws.merge_cells(f'B{project_start_row}:B{current_row - 1}')
merged_cell = ws.cell(row=project_start_row, column=2)
merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
except:
pass
# Merge cells for current sub-project before adding separator
if (current_sub_project is not None and current_sub_project != "-" and
sub_project_start_row is not None and current_row > sub_project_start_row):
try:
# Merge C column (sub-project name)
ws.merge_cells(f'C{sub_project_start_row}:C{current_row - 1}')
merged_cell = ws.cell(row=sub_project_start_row, column=3)
merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
# Merge D column (progress) as well
ws.merge_cells(f'D{sub_project_start_row}:D{current_row - 1}')
merged_cell_d = ws.cell(row=sub_project_start_row, column=4)
merged_cell_d.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
except:
pass
# Add separator row
ws.cell(row=current_row, column=2, value='專案名稱')
for col_idx, header in enumerate(headers[1:], start=3):
ws.cell(row=current_row, column=col_idx, value=header)
# Apply blue header styling to separator row
style_header_row(ws, row_idx=current_row, template_cell=template_header_cell)
current_row += 1
# Reset project tracking for next group
current_project = None
project_start_row = None
current_sub_project = None
sub_project_start_row = None
# Handle the last project merging (if needed)
if current_project is not None and project_start_row is not None and current_row > project_start_row:
try:
ws.merge_cells(f'B{project_start_row}:B{current_row - 1}')
merged_cell = ws.cell(row=project_start_row, column=2)
merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
except:
pass
# Handle the last sub-project merging (if needed)
if (current_sub_project is not None and current_sub_project != "-" and
sub_project_start_row is not None and current_row > sub_project_start_row):
try:
# Merge C column (sub-project name)
ws.merge_cells(f'C{sub_project_start_row}:C{current_row - 1}')
merged_cell = ws.cell(row=sub_project_start_row, column=3)
merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
# Merge D column (progress) as well
ws.merge_cells(f'D{sub_project_start_row}:D{current_row - 1}')
merged_cell_d = ws.cell(row=sub_project_start_row, column=4)
merged_cell_d.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
except:
pass
# Apply formatting (only if no template was used)
if not template_sheet:
# Format title rows
ws['B1'].font = Font(bold=True, size=14)
ws['B1'].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
ws['B2'].font = Font(bold=True, size=12)
ws['B2'].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
ws['B3'].font = Font(size=10, italic=True)
ws['B3'].alignment = Alignment(vertical='top', wrap_text=True)
# Merge cells for headers
ws.merge_cells('B1:J1')
ws.merge_cells('B2:J2')
ws.merge_cells('B3:J3')
# Set column widths
column_widths = {
'B': 20, # 專案名稱
'C': 20, # 子項目名稱
'D': 8, # 進度
'E': 35, # 本周主要進展
'F': 15, # 參與人員
'G': 12, # 工時
'H': 15, # 交付物
'I': 10, # 代碼上傳
'J': 20 # 下周計畫
}
for col, width in column_widths.items():
ws.column_dimensions[col].width = width
# Apply wrap text to all data cells
wrap_align = Alignment(vertical='top', wrap_text=True)
for row in range(5, current_row):
for col in range(2, 11):
ws.cell(row=row, column=col).alignment = wrap_align
# Save workbook
wb.save(output_file)
print(f"\n✓ Weekly report saved to: {output_file}")
return output_file, report_df
if __name__ == "__main__":
# Example usage
import sys
# Default paths
script_dir = os.path.dirname(os.path.abspath(__file__))
if len(sys.argv) > 3:
# Date range provided: start_date end_date output_file
start_date = sys.argv[1]
end_date = sys.argv[2]
output_file = sys.argv[3]
json_input = (start_date, end_date)
elif len(sys.argv) > 2:
# JSON file provided: json_file output_file
json_input = sys.argv[1]
output_file = sys.argv[2]
else:
# Default: use dates and auto-generate output file
from datetime import datetime, timedelta
today = datetime.now()
days_since_monday = today.weekday()
this_monday = today - timedelta(days=days_since_monday)
this_friday = this_monday + timedelta(days=4)
start_date = this_monday.strftime("%Y-%m-%d")
end_date = this_friday.strftime("%Y-%m-%d")
json_input = (start_date, end_date)
timestamp = datetime.now().strftime('%Y%m%d')
output_file = os.path.join(script_dir, 'references', f'項目週報-智能控制組-{timestamp}.xlsx')
# Find template file
template_file = os.path.join(skills_dir, 'week_report_gen', 'references', '項目週報-模板.xlsx')
# Generate the report
try:
output_path, summary_data = generate_weekly_report_from_json(
json_input=json_input,
output_file=output_file,
template_file=template_file if os.path.exists(template_file) else None
)
print("\n" + "="*60)
print("SUMMARY OF PROJECTS:")
print("="*60)
for idx, row in summary_data.iterrows():
print(f"\nProject: {row['專案名稱']}")
print(f" Sub-project: {row['子項目名稱']}")
print(f" Team: {row['參與人員']}")
print(f" Hours: {row['工時']}")
if row['本周主要進展']:
print(f" Progress:\n {row['本周主要進展'].replace(chr(10), chr(10) + ' ')}")
print("\n" + "="*60)
print("✓ Report generation completed successfully!")
print("="*60)
except Exception as e:
print(f"\n✗ Error generating report: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -1,6 +1,6 @@
# Report Generation Skills Experiment
This repository contains AI skills for automated report generation. Currently includes a weekly project report generator that transforms time tracking data into formatted management reports.
This repository contains AI skills for automated report generation. Currently includes weekly project report generators that transform time tracking data into formatted management reports.
## Available Skills
@ -12,6 +12,17 @@ Automatically generates weekly project reports (項目週報) from exported Exce
Fetches and displays time entries grouped by work packages for specified date ranges via OpenProject API. Provides detailed analysis of work package time logging patterns with user, project, and activity information.
### 🚀 Week Report Generator from JSON (`week_report_gen_from_json`)
**New Enhanced Version**: Automatically generates weekly project reports directly from OpenProject API data via JSON format. Features intelligent cell merging, hierarchical project structure recognition, and streamlined workflow.
**Key Advantages:**
- 🔄 **Direct API Integration**: No manual Excel export required
- 🎯 **Smart Cell Merging**: Automatic B/C/D column merging for cleaner presentation
- 📊 **Hierarchical Structure**: Proper project → sub-project → task organization
- ⚡ **Automated Workflow**: From data fetch to formatted report in one command
- 🎨 **Professional Layout**: Enhanced visual grouping and formatting
## Getting Started
### Prerequisites
@ -20,9 +31,25 @@ Fetches and displays time entries grouped by work packages for specified date ra
# Create virtual environment and install dependencies
uv sync
# Verify OpenProject API connection
uv run --env-file .env python -c 'import os; print(os.getenv("OPENPROJECT_TOKEN"))'
```
### Usage Examples
#### Original Excel-based Generator
```bash
uv run --env-file .env python .claude\skills\week_report_gen\generate_report.py "temp\cost-report-2026-01-16-T-16-22-3620260116-7-1r1n4h.xls" "temp\項目週報-智能控制組-20260119.xlsx"
```
#### New JSON-based Generator (Recommended)
```bash
# Generate report for specific date range
uv run --env-file .env python .claude\skills\week_report_gen_from_json\generate_report.py "2026-01-20" "2026-01-22" "temp\週報-智能控制組-20260123.xlsx"
# Generate report for current week (default)
uv run --env-file .env python .claude\skills\week_report_gen_from_json\generate_report.py
# Use existing JSON data file
uv run --env-file .env python .claude\skills\week_report_gen_from_json\generate_report.py "data.json" "output.xlsx"
```