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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user