Aggregate hours and show in table

This commit is contained in:
Кокос Артем Николаевич
2026-01-20 17:01:35 +07:00
parent 910cc31ecf
commit 7f1018a2d4
4 changed files with 38 additions and 25 deletions

View File

@@ -105,9 +105,9 @@ redmine-reporter --compact
╒════════════╤═══════════╤══════════════════════════════════════╤═══════════╤════════════╕ ╒════════════╤═══════════╤══════════════════════════════════════╤═══════════╤════════════╕
│ Проект │ Версия │ Задача │ Статус │ Затрачено │ │ Проект │ Версия │ Задача │ Статус │ Затрачено │
╞════════════╪═══════════╪══════════════════════════════════════╪═══════════╪════════════╡ ╞════════════╪═══════════╪══════════════════════════════════════╪═══════════╪════════════╡
│ Камеры │ v2.5.0 │ 12345. Поддержка нового датчика │ В работе │ │ Камеры │ v2.5.0 │ 12345. Поддержка нового датчика │ В работе │ 2.00h
│ │ │ 12346. Исправить утечку памяти │ Решена │ │ │ │ 12346. Исправить утечку памяти │ Решена │ 2.00h
│ ПО │ <N/A> │ 12350. Обновить документацию │ Ожидание │ │ ПО │ <N/A> │ 12350. Обновить документацию │ Ожидание │ 12.00h
╘════════════╧═══════════╧══════════════════════════════════════╧═══════════╧════════════╛ ╘════════════╧═══════════╧══════════════════════════════════════╧═══════════╧════════════╛
``` ```

View File

@@ -3,7 +3,7 @@ import argparse
from typing import List, Optional from typing import List, Optional
from redminelib.resources import Issue from redminelib.resources import Issue
from .config import Config from .config import Config
from .client import fetch_issues_by_time_entries from .client import fetch_issues_with_spent_time
from .formatter import format_compact, format_table from .formatter import format_compact, format_table
@@ -47,22 +47,22 @@ def main(argv: Optional[List[str]] = None) -> int:
return 1 return 1
try: try:
issues = fetch_issues_by_time_entries(from_date, to_date) issue_hours = fetch_issues_with_spent_time(from_date, to_date)
except Exception as e: except Exception as e:
print(f"❌ Redmine API error: {e}", file=sys.stderr) print(f"❌ Redmine API error: {e}", file=sys.stderr)
return 1 return 1
if issues is None: if issue_hours is None:
print(" No time entries found in the given period.", file=sys.stderr) print(" No time entries found in the given period.", file=sys.stderr)
return 0 return 0
print(f"✅ Total issues: {len(issues)} [{args.date}]") print(f"✅ Total issues: {len(issue_hours)} [{args.date}]")
try: try:
if args.compact: if args.compact:
output = format_compact(issues) output = format_compact(issue_hours)
else: else:
output = format_table(issues) output = format_table(issue_hours)
print(output) print(output)
except Exception as e: except Exception as e:
print(f"❌ Formatting error: {e}", file=sys.stderr) print(f"❌ Formatting error: {e}", file=sys.stderr)

View File

@@ -1,12 +1,14 @@
from typing import List, Optional, Set from typing import List, Optional, Dict, Tuple
from redminelib import Redmine from redminelib import Redmine
from redminelib.resources import Issue from redminelib.resources import Issue
from .config import Config from .config import Config
def fetch_issues_by_time_entries(from_date: str, to_date: str) -> Optional[List[Issue]]: def fetch_issues_with_spent_time(from_date: str, to_date: str) -> Optional[List[Tuple[Issue, float]]]:
""" """
Fetch unique issues linked to time entries of the current user in given date range. Fetch unique issues linked to time entries of the current user in given date range,
along with total spent hours per issue.
Returns list of (issue, total_hours) tuples.
""" """
redmine = Redmine( redmine = Redmine(
@@ -23,15 +25,19 @@ def fetch_issues_by_time_entries(from_date: str, to_date: str) -> Optional[List[
to_date=to_date to_date=to_date
) )
issue_ids: Set[int] = set() # Агрегируем часы по issue.id
spent_time: Dict[int, float] = {}
issue_ids = set()
for entry in time_entries: for entry in time_entries:
if hasattr(entry, 'issue') and entry.issue: if hasattr(entry, 'issue') and entry.issue and hasattr(entry, 'hours'):
issue_ids.add(entry.issue.id) iid = entry.issue.id
issue_ids.add(iid)
spent_time[iid] = spent_time.get(iid, 0.0) + float(entry.hours)
if not issue_ids: if not issue_ids:
return None return None
# Fetch full issue objects with project/version/status # Загружаем полные объекты задач
issue_list_str = ','.join(str(i) for i in issue_ids) issue_list_str = ','.join(str(i) for i in issue_ids)
issues = redmine.issue.filter( issues = redmine.issue.filter(
issue_id=issue_list_str, issue_id=issue_list_str,
@@ -39,4 +45,12 @@ def fetch_issues_by_time_entries(from_date: str, to_date: str) -> Optional[List[
sort='project:asc' sort='project:asc'
) )
return list(issues) # Сопоставляем задачи с суммарным временем
result = []
for issue in issues:
total_hours = spent_time.get(issue.id, 0.0)
result.append((issue, total_hours))
# Сортируем по проекту (Redmine API уже сортирует, но для надёжности)
result.sort(key=lambda x: str(x[0].project))
return result

View File

@@ -1,4 +1,4 @@
from typing import List from typing import List, Tuple
from redminelib.resources import Issue from redminelib.resources import Issue
@@ -19,20 +19,19 @@ def get_version(issue: Issue) -> str:
return str(getattr(issue, 'fixed_version', '<N/A>')) return str(getattr(issue, 'fixed_version', '<N/A>'))
def format_compact(issues: List[Issue]) -> str: def format_compact(issue_hours: List[Tuple[Issue, float]]) -> str:
lines = [] lines = []
prev_project = None prev_project = None
prev_version = None prev_version = None
for issue in issues: for issue, hours in issue_hours:
project = str(issue.project) project = str(issue.project)
version = get_version(issue) version = get_version(issue)
status = str(issue.status) status = str(issue.status)
display_project = project if project != prev_project else "" display_project = project if project != prev_project else ""
display_version = version if (project != prev_project or version != prev_version) else "" display_version = version if (project != prev_project or version != prev_version) else ""
lines.append(f"{display_project} | {display_version} | {issue.id}. {issue.subject} | {status} | {hours:.2f}h")
lines.append(f"{display_project} | {display_version} | {issue.id}. {issue.subject} | {status}")
prev_project = project prev_project = project
prev_version = version prev_version = version
@@ -40,14 +39,14 @@ def format_compact(issues: List[Issue]) -> str:
return "\n".join(lines) return "\n".join(lines)
def format_table(issues: List[Issue]) -> str: def format_table(issue_hours: List[Tuple[Issue, float]]) -> str:
from tabulate import tabulate from tabulate import tabulate
rows = [['Проект', 'Версия', 'Задача', 'Статус', 'Затрачено']] rows = [['Проект', 'Версия', 'Задача', 'Статус', 'Затрачено']]
prev_project = None prev_project = None
prev_version = None prev_version = None
for issue in issues: for issue, hours in issue_hours:
project = str(issue.project) project = str(issue.project)
version = get_version(issue) version = get_version(issue)
status_en = str(issue.status) status_en = str(issue.status)
@@ -61,7 +60,7 @@ def format_table(issues: List[Issue]) -> str:
display_version, display_version,
f"{issue.id}. {issue.subject}", f"{issue.id}. {issue.subject}",
status_ru, status_ru,
"" # placeholder for spent time (future extension) f"{hours:.2f}h"
]) ])
prev_project = project prev_project = project