Fix 9 bugs: ODT covered-cells, CSV BOM, HTML charset, stderr, dead code

- #13 (critical): Add CoveredTableCell elements to ODT for valid row spans (ODF 1.2)
- #28: Move "Total issues" info message from stdout to stderr (clean pipe output)
- #27: Wrap HTML export in full document with DOCTYPE and meta charset utf-8
- #26: Save CSV with utf-8-sig encoding (UTF-8 BOM for Excel compatibility)
- #31: Document CSV uses full project/version values (not display_* like console/MD)
- #30: Fix ODT header formatting when author is empty (no leading dot/space)
- #36: Remove test_cli_smoke_empty testing unreachable code path (return [])
- #37: Remove unused mock_path variable in ODT test fixture
- #34: Remove unreachable len(parts) != 2 check in parse_date_range

Closes #13, #28, #27, #26, #31, #30, #36, #37, #34
This commit is contained in:
Артём Кокос
2026-06-27 13:01:32 +07:00
parent da069993b9
commit 14219564dd
7 changed files with 158 additions and 28 deletions

View File

@@ -15,8 +15,6 @@ def parse_date_range(date_arg: str) -> tuple[str, str]:
if "--" not in date_arg:
raise ValueError("Date range must be in format YYYY-MM-DD--YYYY-MM-DD")
parts = date_arg.split("--", 1)
if len(parts) != 2:
raise ValueError("Invalid date range format")
from_date, to_date = parts[0].strip(), parts[1].strip()
date_pattern = r"\d{4}-\d{2}-\d{2}"
@@ -85,7 +83,7 @@ def main(argv: Optional[List[str]] = None) -> int:
print(" No time entries found in the given period.", file=sys.stderr)
return 0
print(f"✅ Total issues: {len(issue_hours)} [{args.date}]")
print(f"✅ Total issues: {len(issue_hours)} [{args.date}]", file=sys.stderr)
rows = build_grouped_report(issue_hours, fill_time=not args.no_time)

View File

@@ -7,7 +7,13 @@ from .base import Formatter
class CSVFormatter(Formatter):
"""Форматтер для экспорта в CSV."""
"""Форматтер для экспорта в CSV.
Использует полные значения project/version (а не display-значения с пустыми
ячейками для групп). Каждая строка CSV самодостаточна — это корректно для
табличного формата (#31). Файл сохраняется в UTF-8 с BOM (utf-8-sig) для
корректного отображения кириллицы в Microsoft Excel (#26).
"""
def __init__(self, **_kwargs):
super().__init__()
@@ -31,5 +37,5 @@ class CSVFormatter(Formatter):
def save(self, rows: List[ReportRow], output_path: str) -> None:
content = self.format(rows)
with open(output_path, "w", encoding="utf-8", newline="") as f:
with open(output_path, "w", encoding="utf-8-sig", newline="") as f:
f.write(content)

View File

@@ -16,6 +16,12 @@ class HTMLFormatter(Formatter):
projects = group_rows_by_project_and_version(rows)
lines = [
"<!DOCTYPE html>",
'<html lang="ru">',
"<head>",
' <meta charset="utf-8">',
"</head>",
"<body>",
'<table border="1" cellpadding="6" cellspacing="0" style="border-collapse: collapse; font-family: Arial, sans-serif;">',
" <thead>",
" <tr>",
@@ -69,6 +75,8 @@ class HTMLFormatter(Formatter):
lines.append(" </tbody>")
lines.append("</table>")
lines.append("</body>")
lines.append("</html>")
return "\n".join(lines)
def save(self, rows: List[ReportRow], output_path: str) -> None:

View File

@@ -3,7 +3,7 @@ from typing import List
from odf.opendocument import OpenDocument, load
from odf.style import Style, TableCellProperties, TableColumnProperties
from odf.table import Table, TableCell, TableColumn, TableRow
from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow
from odf.text import P
from ..report_builder import group_rows_by_project_and_version
@@ -35,7 +35,10 @@ class ODTFormatter(Formatter):
# Заголовок
month_name = get_month_name_from_range(self.from_date, self.to_date)
header_text = f"{self.author}. Отчет за месяц {month_name}."
if self.author:
header_text = f"{self.author}. Отчет за месяц {month_name}."
else:
header_text = f"Отчет за месяц {month_name}."
doc.text.addElement(P(stylename=para_style_name, text=header_text))
doc.text.addElement(P(stylename=para_style_name, text=""))
@@ -86,15 +89,19 @@ class ODTFormatter(Formatter):
for r in rows_for_version:
row = TableRow()
# Ячейка "Проект" - только в первой строке всего проекта
# Ячейка "Проект" - только в первой строке всего проекта,
# в остальных — covered-cell для валидности ODF (#13)
if first_version_in_project and first_row_in_version:
cell_project = TableCell(stylename=cell_style_name)
cell_project.setAttribute("numberrowsspanned", str(total_project_rows))
p = P(stylename=para_style_name, text=project)
cell_project.addElement(p)
row.addElement(cell_project)
else:
row.addElement(CoveredTableCell())
# Ячейка "Версия" - только в первой строке каждой версии
# Ячейка "Версия" - только в первой строке каждой версии,
# в остальных — covered-cell для валидности ODF (#13)
if first_row_in_version:
cell_version = TableCell(stylename=cell_style_name)
cell_version.setAttribute("numberrowsspanned", str(row_span_version))
@@ -102,6 +109,8 @@ class ODTFormatter(Formatter):
cell_version.addElement(p)
row.addElement(cell_version)
first_row_in_version = False
else:
row.addElement(CoveredTableCell())
# Остальные колонки
task_cell = TableCell(stylename=cell_style_name)