feat: improve project and work package hierarchy handling in week work package logger

This commit is contained in:
MarioYang 2026-01-26 16:55:49 +08:00
parent 0ff400c97c
commit 7f386fce89
1 changed files with 48 additions and 21 deletions

View File

@ -365,6 +365,8 @@ class WeekWorkPackageLogger:
project_trees = {}
# Group entries by project and work package
# Note: project_id here is the direct project associated with the time entry,
# which may not be the root project but a child project in the hierarchy
project_entries = defaultdict(lambda: defaultdict(list))
for entry in time_entries:
@ -382,7 +384,7 @@ class WeekWorkPackageLogger:
# Extract linked information
user_name = self._get_linked_info(entry, 'user', 'title')
project_id = self._get_linked_id(entry, 'project')
project_id = self._get_linked_id(entry, 'project') # This is the actual project from time entry
activity_name = self._get_linked_info(entry, 'activity', 'title')
work_package_id = self._get_linked_id(entry, 'workPackage')
@ -397,6 +399,7 @@ class WeekWorkPackageLogger:
'hours': hours
}
# Group by the direct project from time entry
project_entries[project_id][work_package_id].append(entry_data)
except Exception as e:
@ -405,34 +408,33 @@ class WeekWorkPackageLogger:
# Build tree structure for each project
for project_id, wp_entries in project_entries.items():
# Get the full project hierarchy path from root to this project
project_path = self._get_project_path(project_id)
# Build project hierarchy
# Build project hierarchy - create all projects in the path
current_project_node = None
project_root = None
for proj_id, proj_name in project_path:
if proj_id not in project_trees:
node = TreeNode(proj_id, proj_name, 'project')
project_trees[proj_id] = node
if project_root is None:
project_root = node
project_trees[proj_id] = TreeNode(proj_id, proj_name, 'project')
if current_project_node is None:
# This is the root project in the path
current_project_node = project_trees[proj_id]
project_root = current_project_node
else:
# Link parent to child in the project hierarchy
if project_trees[proj_id] not in current_project_node.children:
current_project_node.add_child(project_trees[proj_id])
current_project_node = project_trees[proj_id]
# If no project path found, create a simple node
# If no project path found (no hierarchy support), create a simple node
if not project_path and project_id:
if project_id not in project_trees:
project_trees[project_id] = TreeNode(project_id, f"Project-{project_id}", 'project')
current_project_node = project_trees[project_id]
# Build work package hierarchy for this project
# current_project_node now points to the actual project where time was logged
wp_nodes = {}
for wp_id, entries in wp_entries.items():
@ -446,6 +448,7 @@ class WeekWorkPackageLogger:
for wp_id_in_path, wp_name in wp_path:
if wp_id_in_path not in wp_nodes:
# Only attach entries to the actual work package (not parent work packages)
node_entries = entries if wp_id_in_path == wp_id else []
node_hours = total_hours if wp_id_in_path == wp_id else 0
wp_nodes[wp_id_in_path] = TreeNode(wp_id_in_path, wp_name, 'work_package', node_hours, node_entries)
@ -457,7 +460,7 @@ class WeekWorkPackageLogger:
current_wp_node.add_child(wp_nodes[wp_id_in_path])
current_wp_node = wp_nodes[wp_id_in_path]
# Attach work package hierarchy to project
# Attach work package hierarchy to the project where time was actually logged
if wp_path and current_project_node:
root_wp_id = wp_path[0][0]
if root_wp_id in wp_nodes:
@ -471,19 +474,24 @@ class WeekWorkPackageLogger:
"""Display tree structure in the specified format"""
# Find root nodes (nodes without parents in our tree structure)
root_nodes = []
all_nodes = set(trees.values())
all_nodes = set()
# Collect all nodes from the trees (including nested children)
def collect_all_nodes(node):
all_nodes.add(node)
for child in node.children:
collect_all_nodes(child)
for node in trees.values():
collect_all_nodes(node)
# Find nodes that don't have parents in the tree
for node in all_nodes:
has_parent_in_tree = False
for other_node in all_nodes:
if node in other_node.children:
has_parent_in_tree = True
break
if not has_parent_in_tree:
if node.parent is None or node.parent not in all_nodes:
root_nodes.append(node)
# Sort root nodes by total hours (descending)
# Remove duplicates and sort root nodes by total hours (descending)
root_nodes = list(set(root_nodes))
root_nodes.sort(key=lambda x: x.get_total_hours(), reverse=True)
print(f"📊 Work Package Time Logging Summary")
@ -524,8 +532,27 @@ class WeekWorkPackageLogger:
def _generate_json_output(self, trees: Dict[str, TreeNode], start_date: str, end_date: str) -> Dict:
"""Generate JSON format output from tree structure"""
# Sort root nodes by total hours (descending)
root_nodes = sorted(trees.values(), key=lambda x: x.get_total_hours(), reverse=True)
# Find root nodes using the same logic as _display_tree
root_nodes = []
all_nodes = set()
# Collect all nodes from the trees (including nested children)
def collect_all_nodes(node):
all_nodes.add(node)
for child in node.children:
collect_all_nodes(child)
for node in trees.values():
collect_all_nodes(node)
# Find nodes that don't have parents in the tree
for node in all_nodes:
if node.parent is None or node.parent not in all_nodes:
root_nodes.append(node)
# Remove duplicates and sort by total hours (descending)
root_nodes = list(set(root_nodes))
root_nodes.sort(key=lambda x: x.get_total_hours(), reverse=True)
# Calculate total hours across all projects
total_hours = sum(node.get_total_hours() for node in root_nodes)