feat: update weekly report generation to handle individual work entries and improve data processing

This commit is contained in:
MarioYang 2026-01-21 14:59:10 +08:00
parent e468848440
commit 15aa43a0e1
2 changed files with 219 additions and 128 deletions

View File

@ -25,7 +25,7 @@ Use this skill when:
Expected columns in the input file:
- `日期` - Date of the work entry
- `使用者` - User/person name
- `活動` - Activity type (Development, Testing, Specification, Support, etc.)
- `登錄為` - Login as type (Development, Testing, Specification, Support, etc.)
- `專案` - Project name
- `留言` - Comments/notes (optional)
- `單位` - Work hours/units
@ -66,7 +66,7 @@ def read_cost_report(file_path):
df = pd.read_excel(file_path, header=1)
# Verify expected columns exist
required_cols = ['日期', '使用者', '活動', '專案', '單位']
required_cols = ['日期', '使用者', '登錄為', '專案', '單位']
if not all(col in df.columns for col in required_cols):
# Try header=0 if header=1 doesn't work
df = pd.read_excel(file_path, header=0)
@ -77,77 +77,78 @@ def read_cost_report(file_path):
return df
```
### Step 2: Aggregate Data by Person and Project
### Step 2: Process Individual Work Entries by Project
```python
def aggregate_work_hours(df):
"""
Group work hours by user and project.
Returns a summary DataFrame.
Process work hours data by project, keeping individual entries separate.
Returns a processed DataFrame and date range.
"""
# Extract date range for report period
df['日期'] = pd.to_datetime(df['日期'])
start_date = df['日期'].min()
end_date = df['日期'].max()
# Group by user and project
summary = df.groupby(['使用者', '專案', '活動']).agg({
'單位': 'sum',
'留言': lambda x: '\n'.join(x.dropna().unique()) if '留言' in df.columns else ''
}).reset_index()
# Keep individual work entries separate (no grouping/aggregation)
summary = df[['使用者', '專案', '單位', '登錄為', '留言']].copy()
summary.columns = ['使用者', '專案', '工時', '登錄為', '備註']
summary.columns = ['使用者', '專案', '活動', '工時', '備註']
# Clean up empty or NaN values
summary['登錄為'] = summary['登錄為'].fillna('')
summary['備註'] = summary['備註'].fillna('')
# Sort by user and hours (descending)
summary = summary.sort_values(['使用者', '工時'], ascending=[True, False])
# Sort by project name and hours (descending)
summary = summary.sort_values(['專案', '工時'], ascending=[True, False])
return summary, start_date, end_date
```
### Step 3: Format Data for Weekly Report
### Step 3: Format Individual Entries for Weekly Report
```python
def format_for_weekly_report(summary_df):
"""
Transform aggregated data into weekly report format.
Group entries by project and list participants.
Transform individual work entries into weekly report format.
Group by project but keep individual work entries separate.
"""
# Group by project
report_data = []
for project in summary_df['專案'].unique():
project_data = summary_df[summary_df['專案'] == project]
# Get all participants and their hours
participants = []
total_hours = 0
activities = []
notes = []
# Process each work entry individually
for _, row in project_data.iterrows():
participants.append(f"{row['使用者']}")
total_hours += row['工時']
if row['活動'] and row['活動'] != '-':
activities.append(row['活動'])
user_name = row['使用者']
user_hours = row['工時']
# Format progress text from activity and notes
progress_parts = []
if row['登錄為'] and row['登錄為'] != '-':
# Extract project name after the colon from format like "Task #838: AXEL CommLib 重構"
login_as_text = str(row['登錄為'])
if ':' in login_as_text:
project_name = login_as_text.split(':', 1)[1].strip()
progress_parts.append(f"{project_name}")
else:
progress_parts.append(f"{login_as_text}")
if row['備註']:
notes.append(row['備註'])
# Format main progress
progress_text = '\n'.join(set(notes)) if notes else ''
if activities:
activity_text = '\n'.join(set(activities))
progress_text = f"{activity_text}\n{progress_text}" if progress_text else activity_text
report_data.append({
'專案名稱': project,
'子項目名稱': None,
'進度': None, # User should fill this in
'本周主要進展': progress_text,
'參與人員': ', '.join(set(participants)),
'工時': total_hours,
'交付物': None,
'代碼上傳': None,
'下周計畫': None
progress_parts.append(str(row['備註']))
progress_text = ' '.join(progress_parts) if progress_parts else None
report_data.append({
'專案名稱': project,
'子項目名稱': None,
'進度': None, # User should fill this in
'本周主要進展': progress_text,
'參與人員': user_name,
'工時': user_hours,
'交付物': None,
'代碼上傳': None,
'下周計畫': None
})
return pd.DataFrame(report_data)

View File

@ -153,31 +153,39 @@ def read_cost_report(file_path):
def aggregate_work_hours(df):
"""
Group work hours by user and project.
Returns a summary DataFrame and date range.
Process work hours data by project, keeping individual entries separate.
Returns a processed DataFrame and date range.
"""
# Extract date range for report period
df['日期'] = pd.to_datetime(df['日期'])
start_date = df['日期'].min()
end_date = df['日期'].max()
# Group by user and project
agg_dict = {'單位': 'sum'}
# Prepare the summary DataFrame with individual entries
summary_columns = ['使用者', '專案', '工時']
# Add 留言 (comments) if column exists
if '留言' in df.columns:
agg_dict['留言'] = lambda x: '\n'.join([str(i) for i in x.dropna().unique() if str(i) != 'nan'])
# Add 活動 (activity) if column exists
if '活動' in df.columns:
summary = df.groupby(['使用者', '專案', '活動']).agg(agg_dict).reset_index()
summary.columns = ['使用者', '專案', '活動', '工時'] + (['備註'] if '留言' in df.columns else [])
# Add 登錄為 (login as) column if exists
if '登錄為' in df.columns:
summary_columns.append('登錄為')
else:
summary = df.groupby(['使用者', '專案']).agg(agg_dict).reset_index()
summary.columns = ['使用者', '專案', '工時'] + (['備註'] if '留言' in df.columns else [])
summary['活動'] = ''
df['登錄為'] = ''
summary_columns.append('登錄為')
# Sort by user and hours (descending)
# Add 備註 (comments) column if exists
if '留言' in df.columns:
summary_columns.append('備註')
summary = df[['使用者', '專案', '單位', '登錄為', '留言']].copy()
summary.columns = ['使用者', '專案', '工時', '登錄為', '備註']
else:
summary = df[['使用者', '專案', '單位', '登錄為']].copy()
summary.columns = ['使用者', '專案', '工時', '登錄為']
# Clean up empty or NaN values
summary['登錄為'] = summary['登錄為'].fillna('')
if '備註' in summary.columns:
summary['備註'] = summary['備註'].fillna('')
# Sort by project name first, then by hours (descending) within each project
summary = summary.sort_values(['專案', '工時'], ascending=[True, False])
return summary, start_date, end_date
@ -185,90 +193,123 @@ def aggregate_work_hours(df):
def format_for_weekly_report(summary_df, user_manager=None, project_manager=None):
"""
Transform aggregated data into weekly report format.
Group entries by project and list participants.
Transform individual work entries into weekly report format.
Group by project and merge entries with same (user, task name) but different comments.
Uses project hierarchy to organize parent-child project relationships.
Args:
summary_df: Aggregated work data
summary_df: Individual work data entries
user_manager: Optional UserManager instance for name mapping
project_manager: Optional ProjectManager instance for hierarchy
"""
report_data = []
# Process projects in order to maintain grouping
for project in summary_df['專案'].unique():
project_data = summary_df[summary_df['專案'] == project]
# Get all participants and their hours
participants = []
total_hours = 0
activities = []
notes = []
# Group by user and task name for merging
grouped_entries = {}
# First pass: collect and group entries
for _, row in project_data.iterrows():
user_name = row['使用者']
user_hours = row['工時']
# 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')
participants.append(formal_name)
participant = formal_name
else:
participants.append(f"{user_name}")
participant = user_name
total_hours += user_hours
# Extract task name from 登錄為
task_name = None
if '登錄為' in row and row['登錄為'] and row['登錄為'] != '-' and pd.notna(row['登錄為']):
login_as_text = str(row['登錄為'])
if ':' in login_as_text:
task_name = login_as_text.split(':', 1)[1].strip()
else:
task_name = login_as_text
if '活動' in row and row['活動'] and row['活動'] != '-' and pd.notna(row['活動']):
activities.append(row['活動'])
# Create grouping key
group_key = (participant, task_name)
# Initialize group if not exists
if group_key not in grouped_entries:
grouped_entries[group_key] = {
'participant': participant,
'task_name': task_name,
'total_hours': 0,
'comments': [],
'project': project
}
# Add to group
grouped_entries[group_key]['total_hours'] += row['工時']
# Add comment if exists and not already added
if '備註' in row and row['備註'] and pd.notna(row['備註']):
notes.append(row['備註'])
comment = str(row['備註']).strip()
if comment and comment != "-" and comment not in grouped_entries[group_key]['comments']:
grouped_entries[group_key]['comments'].append(comment)
# Format main progress
progress_text = ''
if activities:
activity_text = '\n'.join([f"{act}" for act in set(activities)])
progress_text = activity_text
if notes:
note_text = '\n'.join(set(notes))
progress_text = f"{progress_text}\n{note_text}" if progress_text else note_text
# Determine if this is a sub-project using project hierarchy
parent_project = None
project_name = project
sub_project_name = None
if project_manager:
# Try to find the project by exact name match
project_id = project_manager.find_project_by_name(project)
# Second pass: generate report entries from grouped data
for group_key, group_data in grouped_entries.items():
participant = group_data['participant']
task_name = group_data['task_name']
total_hours = group_data['total_hours']
comments = group_data['comments']
if project_id:
parent_id = project_manager.get_parent(project_id)
# Format progress text
progress_parts = []
if task_name:
progress_parts.append(f"{task_name}")
# Format comments as bullet points if multiple
if comments:
if len(comments) == 1:
progress_parts.append(comments[0])
else:
# Multiple comments - format as bullet list
bullet_comments = '\n'.join([f" - {comment}" for comment in comments])
progress_parts.append(f"\n{bullet_comments}")
progress_text = ' '.join(progress_parts) if progress_parts else None
# Determine if this is a sub-project using project hierarchy
parent_project = None
project_name = project
sub_project_name = None
if project_manager:
# Try to find the project by exact name match
project_id = project_manager.find_project_by_name(project)
if parent_id:
# This is a sub-project
parent_info = project_manager.get_project(parent_id)
if parent_info:
parent_project = parent_info['name']
sub_project_name = project
project_name = parent_project
else:
# Project not found in hierarchy, log for debugging
pass
report_data.append({
'專案名稱': project_name,
'子項目名稱': sub_project_name,
'進度': None, # User should fill this in
'本周主要進展': progress_text if progress_text else None,
'參與人員': ', '.join(set(participants)),
'工時': total_hours,
'交付物': None,
'代碼上傳': None,
'下周計畫': None
})
if project_id:
parent_id = project_manager.get_parent(project_id)
if parent_id:
# This is a sub-project
parent_info = project_manager.get_project(parent_id)
if parent_info:
parent_project = parent_info['name']
sub_project_name = project
project_name = parent_project
else:
# Project not found in hierarchy, log for debugging
pass
report_data.append({
'專案名稱': project_name,
'子項目名稱': sub_project_name,
'進度': None, # User should fill this in
'本周主要進展': progress_text if progress_text else None,
'參與人員': participant,
'工時': total_hours,
'交付物': None,
'代碼上傳': None,
'下周計畫': None
})
return pd.DataFrame(report_data)
@ -318,10 +359,10 @@ def generate_weekly_report(input_file, output_file, template_file=None, team_nam
summary, start_date, end_date = aggregate_work_hours(df)
print(f"Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
print(f"Found {len(summary)} work entries across {summary['專案'].nunique()} projects")
print(f"Found {len(summary)} individual work entries across {summary['專案'].nunique()} projects")
report_df = format_for_weekly_report(summary, user_manager=user_manager, project_manager=project_manager)
print(f"Formatted {len(report_df)} project entries for report")
print(f"Formatted {len(report_df)} individual work entries for report")
# Load template or create new workbook
if template_file and os.path.exists(template_file):
@ -378,8 +419,31 @@ def generate_weekly_report(input_file, output_file, template_file=None, team_nam
# Write data starting from row 5
current_row = 5
project_start_rows = {} # Track where each project starts for merging
current_project = None
project_start_row = None
for idx, row_data in report_df.iterrows():
ws.cell(row=current_row, column=2, value=row_data['專案名稱'])
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
# Start tracking new project
current_project = project_name
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['本周主要進展'])
@ -396,12 +460,38 @@ def generate_weekly_report(input_file, output_file, template_file=None, team_nam
# Add separator row (except after last entry)
if idx < len(report_df) - 1:
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 (no blank white spacer)
style_header_row(ws, row_idx=current_row, template_cell=template_header_cell)
current_row += 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
# 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 (no blank white spacer)
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
# 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
# Apply formatting (only if no template was used)
if not template_sheet: