From 7f1018a2d46fa76010b65cf2358de4711383d677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=BA=D0=BE=D1=81=20=D0=90=D1=80=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B5=D0=B2=D0=B8?= =?UTF-8?q?=D1=87?= Date: Tue, 20 Jan 2026 17:01:35 +0700 Subject: [PATCH] Aggregate hours and show in table --- README.md | 6 +++--- redmine_reporter/cli.py | 12 ++++++------ redmine_reporter/client.py | 30 ++++++++++++++++++++++-------- redmine_reporter/formatter.py | 15 +++++++-------- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3f68ce6..c84c55d 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,9 @@ redmine-reporter --compact ╒════════════╤═══════════╤══════════════════════════════════════╤═══════════╤════════════╕ │ Проект │ Версия │ Задача │ Статус │ Затрачено │ ╞════════════╪═══════════╪══════════════════════════════════════╪═══════════╪════════════╡ -│ Камеры │ v2.5.0 │ 12345. Поддержка нового датчика │ В работе │ │ -│ │ │ 12346. Исправить утечку памяти │ Решена │ │ -│ ПО │ │ 12350. Обновить документацию │ Ожидание │ │ +│ Камеры │ v2.5.0 │ 12345. Поддержка нового датчика │ В работе │ 2.00h │ +│ │ │ 12346. Исправить утечку памяти │ Решена │ 2.00h │ +│ ПО │ │ 12350. Обновить документацию │ Ожидание │ 12.00h │ ╘════════════╧═══════════╧══════════════════════════════════════╧═══════════╧════════════╛ ``` diff --git a/redmine_reporter/cli.py b/redmine_reporter/cli.py index 7166db4..936f2fd 100644 --- a/redmine_reporter/cli.py +++ b/redmine_reporter/cli.py @@ -3,7 +3,7 @@ import argparse from typing import List, Optional from redminelib.resources import Issue 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 @@ -47,22 +47,22 @@ def main(argv: Optional[List[str]] = None) -> int: return 1 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: print(f"❌ Redmine API error: {e}", file=sys.stderr) return 1 - if issues is None: + if issue_hours is None: print("ℹ️ No time entries found in the given period.", file=sys.stderr) return 0 - print(f"✅ Total issues: {len(issues)} [{args.date}]") + print(f"✅ Total issues: {len(issue_hours)} [{args.date}]") try: if args.compact: - output = format_compact(issues) + output = format_compact(issue_hours) else: - output = format_table(issues) + output = format_table(issue_hours) print(output) except Exception as e: print(f"❌ Formatting error: {e}", file=sys.stderr) diff --git a/redmine_reporter/client.py b/redmine_reporter/client.py index 80e1b87..5503cd2 100644 --- a/redmine_reporter/client.py +++ b/redmine_reporter/client.py @@ -1,12 +1,14 @@ -from typing import List, Optional, Set +from typing import List, Optional, Dict, Tuple from redminelib import Redmine from redminelib.resources import Issue 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( @@ -23,15 +25,19 @@ def fetch_issues_by_time_entries(from_date: str, to_date: str) -> Optional[List[ to_date=to_date ) - issue_ids: Set[int] = set() + # Агрегируем часы по issue.id + spent_time: Dict[int, float] = {} + issue_ids = set() for entry in time_entries: - if hasattr(entry, 'issue') and entry.issue: - issue_ids.add(entry.issue.id) + if hasattr(entry, 'issue') and entry.issue and hasattr(entry, 'hours'): + iid = entry.issue.id + issue_ids.add(iid) + spent_time[iid] = spent_time.get(iid, 0.0) + float(entry.hours) if not issue_ids: return None - # Fetch full issue objects with project/version/status + # Загружаем полные объекты задач issue_list_str = ','.join(str(i) for i in issue_ids) issues = redmine.issue.filter( 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' ) - 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 diff --git a/redmine_reporter/formatter.py b/redmine_reporter/formatter.py index afdf8cf..06d1aa0 100644 --- a/redmine_reporter/formatter.py +++ b/redmine_reporter/formatter.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Tuple from redminelib.resources import Issue @@ -19,20 +19,19 @@ def get_version(issue: Issue) -> str: return str(getattr(issue, 'fixed_version', '')) -def format_compact(issues: List[Issue]) -> str: +def format_compact(issue_hours: List[Tuple[Issue, float]]) -> str: lines = [] prev_project = None prev_version = None - for issue in issues: + for issue, hours in issue_hours: project = str(issue.project) version = get_version(issue) status = str(issue.status) display_project = project if project != prev_project 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}") + lines.append(f"{display_project} | {display_version} | {issue.id}. {issue.subject} | {status} | {hours:.2f}h") prev_project = project prev_version = version @@ -40,14 +39,14 @@ def format_compact(issues: List[Issue]) -> str: 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 rows = [['Проект', 'Версия', 'Задача', 'Статус', 'Затрачено']] prev_project = None prev_version = None - for issue in issues: + for issue, hours in issue_hours: project = str(issue.project) version = get_version(issue) status_en = str(issue.status) @@ -61,7 +60,7 @@ def format_table(issues: List[Issue]) -> str: display_version, f"{issue.id}. {issue.subject}", status_ru, - "" # placeholder for spent time (future extension) + f"{hours:.2f}h" ]) prev_project = project