forked from Zhubei_iXD/report_skill_expm
feat: update weekly report generation to handle individual work entries and improve data processing
This commit is contained in:
parent
e468848440
commit
15aa43a0e1
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue