Time Log Report fixes
* Time reports: Fix report 'group by' logic to not show ambiguous 'note' data. * Time reports: Add default option to 'don't group'. This gives every individual time log row (and so the 'note' is shown in this case)
This commit is contained in:
parent
2464147a59
commit
778d988ebd
5 changed files with 225 additions and 35 deletions
|
|
@ -1,3 +1,8 @@
|
||||||
|
# 0.6.4
|
||||||
|
|
||||||
|
* Time reports: Fix report 'group by' logic to not show ambiguous 'note' data.
|
||||||
|
* Time reports: Add default option to 'don't group'. This gives every individual time log row (and so the 'note' is shown in this case)
|
||||||
|
|
||||||
# 0.6.3
|
# 0.6.3
|
||||||
|
|
||||||
* Allow 'this week', 'this month', 'this year' granularity in Timesheet reports. Default date range to start from this month.
|
* Allow 'this week', 'this month', 'this year' granularity in Timesheet reports. Default date range to start from this month.
|
||||||
|
|
|
||||||
|
|
@ -1108,8 +1108,8 @@ class DBManager:
|
||||||
project_id: int,
|
project_id: int,
|
||||||
start_date_iso: str,
|
start_date_iso: str,
|
||||||
end_date_iso: str,
|
end_date_iso: str,
|
||||||
granularity: str = "day", # 'day' | 'week' | 'month'
|
granularity: str = "day", # 'day' | 'week' | 'month' | 'none'
|
||||||
) -> list[tuple[str, str, int]]:
|
) -> list[tuple[str, str, str, int]]:
|
||||||
"""
|
"""
|
||||||
Return (time_period, activity_name, total_minutes) tuples between start and end
|
Return (time_period, activity_name, total_minutes) tuples between start and end
|
||||||
for a project, grouped by period and activity.
|
for a project, grouped by period and activity.
|
||||||
|
|
@ -1117,7 +1117,33 @@ class DBManager:
|
||||||
- 'YYYY-MM-DD' for day
|
- 'YYYY-MM-DD' for day
|
||||||
- 'YYYY-WW' for week
|
- 'YYYY-WW' for week
|
||||||
- 'YYYY-MM' for month
|
- 'YYYY-MM' for month
|
||||||
|
For 'none' granularity, each individual time log entry becomes a row.
|
||||||
"""
|
"""
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
|
||||||
|
if granularity == "none":
|
||||||
|
# No grouping: one row per entry
|
||||||
|
rows = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
t.page_date AS period,
|
||||||
|
a.name AS activity_name,
|
||||||
|
t.note AS note,
|
||||||
|
t.minutes AS total_minutes
|
||||||
|
FROM time_log t
|
||||||
|
JOIN activities a ON a.id = t.activity_id
|
||||||
|
WHERE t.project_id = ?
|
||||||
|
AND t.page_date BETWEEN ? AND ?
|
||||||
|
ORDER BY period, LOWER(a.name), t.id;
|
||||||
|
""",
|
||||||
|
(project_id, start_date_iso, end_date_iso),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
return [
|
||||||
|
(r["period"], r["activity_name"], r["note"], r["total_minutes"])
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
if granularity == "day":
|
if granularity == "day":
|
||||||
bucket_expr = "page_date"
|
bucket_expr = "page_date"
|
||||||
elif granularity == "week":
|
elif granularity == "week":
|
||||||
|
|
@ -1126,13 +1152,11 @@ class DBManager:
|
||||||
else: # month
|
else: # month
|
||||||
bucket_expr = "substr(page_date, 1, 7)" # YYYY-MM
|
bucket_expr = "substr(page_date, 1, 7)" # YYYY-MM
|
||||||
|
|
||||||
cur = self.conn.cursor()
|
|
||||||
rows = cur.execute(
|
rows = cur.execute(
|
||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
{bucket_expr} AS bucket,
|
{bucket_expr} AS bucket,
|
||||||
a.name AS activity_name,
|
a.name AS activity_name,
|
||||||
t.note AS note,
|
|
||||||
SUM(t.minutes) AS total_minutes
|
SUM(t.minutes) AS total_minutes
|
||||||
FROM time_log t
|
FROM time_log t
|
||||||
JOIN activities a ON a.id = t.activity_id
|
JOIN activities a ON a.id = t.activity_id
|
||||||
|
|
@ -1144,21 +1168,50 @@ class DBManager:
|
||||||
(project_id, start_date_iso, end_date_iso),
|
(project_id, start_date_iso, end_date_iso),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
return [
|
return [(r["bucket"], r["activity_name"], "", r["total_minutes"]) for r in rows]
|
||||||
(r["bucket"], r["activity_name"], r["note"], r["total_minutes"])
|
|
||||||
for r in rows
|
|
||||||
]
|
|
||||||
|
|
||||||
def time_report_all(
|
def time_report_all(
|
||||||
self,
|
self,
|
||||||
start_date_iso: str,
|
start_date_iso: str,
|
||||||
end_date_iso: str,
|
end_date_iso: str,
|
||||||
granularity: str = "day", # 'day' | 'week' | 'month'
|
granularity: str = "day", # 'day' | 'week' | 'month' | 'none'
|
||||||
) -> list[tuple[str, str, str, str, int]]:
|
) -> list[tuple[str, str, str, str, int]]:
|
||||||
"""
|
"""
|
||||||
Return (project_name, time_period, activity_name, note, total_minutes)
|
Return (project_name, time_period, activity_name, note, total_minutes)
|
||||||
across *all* projects between start and end, grouped by project + period + activity.
|
across *all* projects between start and end, grouped by project + period + activity.
|
||||||
"""
|
"""
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
|
||||||
|
if granularity == "none":
|
||||||
|
# No grouping – one row per time_log record
|
||||||
|
rows = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
p.name AS project_name,
|
||||||
|
t.page_date AS period,
|
||||||
|
a.name AS activity_name,
|
||||||
|
t.note AS note,
|
||||||
|
t.minutes AS total_minutes
|
||||||
|
FROM time_log t
|
||||||
|
JOIN projects p ON p.id = t.project_id
|
||||||
|
JOIN activities a ON a.id = t.activity_id
|
||||||
|
WHERE t.page_date BETWEEN ? AND ?
|
||||||
|
ORDER BY LOWER(p.name), period, LOWER(activity_name), t.id;
|
||||||
|
""",
|
||||||
|
(start_date_iso, end_date_iso),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
r["project_name"],
|
||||||
|
r["period"],
|
||||||
|
r["activity_name"],
|
||||||
|
r["note"],
|
||||||
|
r["total_minutes"],
|
||||||
|
)
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
if granularity == "day":
|
if granularity == "day":
|
||||||
bucket_expr = "page_date"
|
bucket_expr = "page_date"
|
||||||
elif granularity == "week":
|
elif granularity == "week":
|
||||||
|
|
@ -1166,14 +1219,12 @@ class DBManager:
|
||||||
else: # month
|
else: # month
|
||||||
bucket_expr = "substr(page_date, 1, 7)" # YYYY-MM
|
bucket_expr = "substr(page_date, 1, 7)" # YYYY-MM
|
||||||
|
|
||||||
cur = self.conn.cursor()
|
|
||||||
rows = cur.execute(
|
rows = cur.execute(
|
||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
p.name AS project_name,
|
p.name AS project_name,
|
||||||
{bucket_expr} AS bucket,
|
{bucket_expr} AS bucket,
|
||||||
a.name AS activity_name,
|
a.name AS activity_name,
|
||||||
t.note AS note,
|
|
||||||
SUM(t.minutes) AS total_minutes
|
SUM(t.minutes) AS total_minutes
|
||||||
FROM time_log t
|
FROM time_log t
|
||||||
JOIN projects p ON p.id = t.project_id
|
JOIN projects p ON p.id = t.project_id
|
||||||
|
|
@ -1190,7 +1241,7 @@ class DBManager:
|
||||||
r["project_name"],
|
r["project_name"],
|
||||||
r["bucket"],
|
r["bucket"],
|
||||||
r["activity_name"],
|
r["activity_name"],
|
||||||
r["note"],
|
"",
|
||||||
r["total_minutes"],
|
r["total_minutes"],
|
||||||
)
|
)
|
||||||
for r in rows
|
for r in rows
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@
|
||||||
"add_project": "Add project",
|
"add_project": "Add project",
|
||||||
"add_time_entry": "Add time entry",
|
"add_time_entry": "Add time entry",
|
||||||
"time_period": "Time period",
|
"time_period": "Time period",
|
||||||
|
"dont_group": "Don't group",
|
||||||
"by_day": "by day",
|
"by_day": "by day",
|
||||||
"by_month": "by month",
|
"by_month": "by month",
|
||||||
"by_week": "by week",
|
"by_week": "by week",
|
||||||
|
|
|
||||||
|
|
@ -986,7 +986,7 @@ class TimeReportDialog(QDialog):
|
||||||
self._db = db
|
self._db = db
|
||||||
|
|
||||||
# state for last run
|
# state for last run
|
||||||
self._last_rows: list[tuple[str, str, int]] = []
|
self._last_rows: list[tuple[str, str, str, str, int]] = []
|
||||||
self._last_total_minutes: int = 0
|
self._last_total_minutes: int = 0
|
||||||
self._last_project_name: str = ""
|
self._last_project_name: str = ""
|
||||||
self._last_start: str = ""
|
self._last_start: str = ""
|
||||||
|
|
@ -1038,6 +1038,7 @@ class TimeReportDialog(QDialog):
|
||||||
|
|
||||||
# Granularity
|
# Granularity
|
||||||
self.granularity = QComboBox()
|
self.granularity = QComboBox()
|
||||||
|
self.granularity.addItem(strings._("dont_group"), "none")
|
||||||
self.granularity.addItem(strings._("by_day"), "day")
|
self.granularity.addItem(strings._("by_day"), "day")
|
||||||
self.granularity.addItem(strings._("by_week"), "week")
|
self.granularity.addItem(strings._("by_week"), "week")
|
||||||
self.granularity.addItem(strings._("by_month"), "month")
|
self.granularity.addItem(strings._("by_month"), "month")
|
||||||
|
|
@ -1095,6 +1096,43 @@ class TimeReportDialog(QDialog):
|
||||||
close_row.addWidget(close_btn)
|
close_row.addWidget(close_btn)
|
||||||
root.addLayout(close_row)
|
root.addLayout(close_row)
|
||||||
|
|
||||||
|
def _configure_table_columns(self, granularity: str) -> None:
|
||||||
|
if granularity == "none":
|
||||||
|
# Show notes
|
||||||
|
self.table.setColumnCount(5)
|
||||||
|
self.table.setHorizontalHeaderLabels(
|
||||||
|
[
|
||||||
|
strings._("project"),
|
||||||
|
strings._("time_period"),
|
||||||
|
strings._("activity"),
|
||||||
|
strings._("note"),
|
||||||
|
strings._("hours"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# project, period, activity, note stretch; hours shrink
|
||||||
|
header = self.table.horizontalHeader()
|
||||||
|
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
||||||
|
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
||||||
|
header.setSectionResizeMode(3, QHeaderView.Stretch)
|
||||||
|
header.setSectionResizeMode(4, QHeaderView.ResizeToContents)
|
||||||
|
else:
|
||||||
|
# Grouped: no note column
|
||||||
|
self.table.setColumnCount(4)
|
||||||
|
self.table.setHorizontalHeaderLabels(
|
||||||
|
[
|
||||||
|
strings._("project"),
|
||||||
|
strings._("time_period"),
|
||||||
|
strings._("activity"),
|
||||||
|
strings._("hours"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
header = self.table.horizontalHeader()
|
||||||
|
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
||||||
|
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
||||||
|
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||||
|
|
||||||
def _on_range_preset_changed(self, index: int) -> None:
|
def _on_range_preset_changed(self, index: int) -> None:
|
||||||
preset = self.range_preset.currentData()
|
preset = self.range_preset.currentData()
|
||||||
today = QDate.currentDate()
|
today = QDate.currentDate()
|
||||||
|
|
@ -1140,6 +1178,9 @@ class TimeReportDialog(QDialog):
|
||||||
self._last_start = start
|
self._last_start = start
|
||||||
self._last_end = end
|
self._last_end = end
|
||||||
self._last_gran_label = self.granularity.currentText()
|
self._last_gran_label = self.granularity.currentText()
|
||||||
|
self._last_gran = gran # remember which grouping was used
|
||||||
|
|
||||||
|
self._configure_table_columns(gran)
|
||||||
|
|
||||||
rows_for_table: list[tuple[str, str, str, str, int]] = []
|
rows_for_table: list[tuple[str, str, str, str, int]] = []
|
||||||
|
|
||||||
|
|
@ -1179,8 +1220,13 @@ class TimeReportDialog(QDialog):
|
||||||
self.table.setItem(i, 0, QTableWidgetItem(project))
|
self.table.setItem(i, 0, QTableWidgetItem(project))
|
||||||
self.table.setItem(i, 1, QTableWidgetItem(time_period))
|
self.table.setItem(i, 1, QTableWidgetItem(time_period))
|
||||||
self.table.setItem(i, 2, QTableWidgetItem(activity_name))
|
self.table.setItem(i, 2, QTableWidgetItem(activity_name))
|
||||||
self.table.setItem(i, 3, QTableWidgetItem(note))
|
|
||||||
|
if self._last_gran == "none":
|
||||||
|
self.table.setItem(i, 3, QTableWidgetItem(note or ""))
|
||||||
self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}"))
|
self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}"))
|
||||||
|
else:
|
||||||
|
# no note column
|
||||||
|
self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}"))
|
||||||
|
|
||||||
# Summary label – include per-project totals when in "all projects" mode
|
# Summary label – include per-project totals when in "all projects" mode
|
||||||
total_hours = self._last_total_minutes / 60.0
|
total_hours = self._last_total_minutes / 60.0
|
||||||
|
|
@ -1224,16 +1270,18 @@ class TimeReportDialog(QDialog):
|
||||||
with open(filename, "w", newline="", encoding="utf-8") as f:
|
with open(filename, "w", newline="", encoding="utf-8") as f:
|
||||||
writer = csv.writer(f)
|
writer = csv.writer(f)
|
||||||
|
|
||||||
|
show_note = getattr(self, "_last_gran", "day") == "none"
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
writer.writerow(
|
header = [
|
||||||
[
|
|
||||||
strings._("project"),
|
strings._("project"),
|
||||||
strings._("time_period"),
|
strings._("time_period"),
|
||||||
strings._("activity"),
|
strings._("activity"),
|
||||||
strings._("note"),
|
|
||||||
strings._("hours"),
|
|
||||||
]
|
]
|
||||||
)
|
if show_note:
|
||||||
|
header.append(strings._("note"))
|
||||||
|
header.append(strings._("hours"))
|
||||||
|
writer.writerow(header)
|
||||||
|
|
||||||
# Data rows
|
# Data rows
|
||||||
for (
|
for (
|
||||||
|
|
@ -1244,9 +1292,11 @@ class TimeReportDialog(QDialog):
|
||||||
minutes,
|
minutes,
|
||||||
) in self._last_rows:
|
) in self._last_rows:
|
||||||
hours = minutes / 60.0
|
hours = minutes / 60.0
|
||||||
writer.writerow(
|
row = [project, time_period, activity_name]
|
||||||
[project, time_period, activity_name, note, f"{hours:.2f}"]
|
if show_note:
|
||||||
)
|
row.append(note)
|
||||||
|
row.append(f"{hours:.2f}")
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
# Blank line + total
|
# Blank line + total
|
||||||
total_hours = self._last_total_minutes / 60.0
|
total_hours = self._last_total_minutes / 60.0
|
||||||
|
|
|
||||||
|
|
@ -1191,7 +1191,7 @@ def test_time_report_dialog_creation(qtbot, fresh_db):
|
||||||
qtbot.addWidget(dialog)
|
qtbot.addWidget(dialog)
|
||||||
|
|
||||||
assert dialog.project_combo.count() == 1
|
assert dialog.project_combo.count() == 1
|
||||||
assert dialog.granularity.count() == 3 # day, week, month
|
assert dialog.granularity.count() == 4
|
||||||
|
|
||||||
|
|
||||||
def test_time_report_dialog_loads_projects(qtbot, fresh_db):
|
def test_time_report_dialog_loads_projects(qtbot, fresh_db):
|
||||||
|
|
@ -1230,7 +1230,9 @@ def test_time_report_dialog_run_report(qtbot, fresh_db):
|
||||||
dialog.project_combo.setCurrentIndex(0)
|
dialog.project_combo.setCurrentIndex(0)
|
||||||
dialog.from_date.setDate(QDate.fromString(_today(), "yyyy-MM-dd"))
|
dialog.from_date.setDate(QDate.fromString(_today(), "yyyy-MM-dd"))
|
||||||
dialog.to_date.setDate(QDate.fromString(_today(), "yyyy-MM-dd"))
|
dialog.to_date.setDate(QDate.fromString(_today(), "yyyy-MM-dd"))
|
||||||
dialog.granularity.setCurrentIndex(0) # day
|
idx_day = dialog.granularity.findData("day")
|
||||||
|
assert idx_day != -1
|
||||||
|
dialog.granularity.setCurrentIndex(idx_day)
|
||||||
|
|
||||||
dialog._run_report()
|
dialog._run_report()
|
||||||
|
|
||||||
|
|
@ -1417,13 +1419,18 @@ def test_time_report_dialog_granularity_week(qtbot, fresh_db):
|
||||||
dialog.project_combo.setCurrentIndex(0)
|
dialog.project_combo.setCurrentIndex(0)
|
||||||
dialog.from_date.setDate(QDate.fromString(date1, "yyyy-MM-dd"))
|
dialog.from_date.setDate(QDate.fromString(date1, "yyyy-MM-dd"))
|
||||||
dialog.to_date.setDate(QDate.fromString(date2, "yyyy-MM-dd"))
|
dialog.to_date.setDate(QDate.fromString(date2, "yyyy-MM-dd"))
|
||||||
dialog.granularity.setCurrentIndex(1) # week
|
|
||||||
|
idx_week = dialog.granularity.findData("week")
|
||||||
|
assert idx_week != -1
|
||||||
|
dialog.granularity.setCurrentIndex(idx_week)
|
||||||
|
|
||||||
dialog._run_report()
|
dialog._run_report()
|
||||||
|
|
||||||
# Should aggregate to single week
|
# Should aggregate to single week
|
||||||
assert dialog.table.rowCount() == 1
|
assert dialog.table.rowCount() == 1
|
||||||
hours_text = dialog.table.item(0, 4).text()
|
# In grouped modes the Note column is hidden → hours are in column 3
|
||||||
|
hours_text = dialog.table.item(0, 3).text()
|
||||||
|
|
||||||
assert "2.5" in hours_text or "2.50" in hours_text
|
assert "2.5" in hours_text or "2.50" in hours_text
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1445,13 +1452,17 @@ def test_time_report_dialog_granularity_month(qtbot, fresh_db):
|
||||||
dialog.project_combo.setCurrentIndex(0)
|
dialog.project_combo.setCurrentIndex(0)
|
||||||
dialog.from_date.setDate(QDate.fromString(date1, "yyyy-MM-dd"))
|
dialog.from_date.setDate(QDate.fromString(date1, "yyyy-MM-dd"))
|
||||||
dialog.to_date.setDate(QDate.fromString(date2, "yyyy-MM-dd"))
|
dialog.to_date.setDate(QDate.fromString(date2, "yyyy-MM-dd"))
|
||||||
dialog.granularity.setCurrentIndex(2) # month
|
|
||||||
|
idx_month = dialog.granularity.findData("month")
|
||||||
|
assert idx_month != -1
|
||||||
|
dialog.granularity.setCurrentIndex(idx_month)
|
||||||
|
|
||||||
dialog._run_report()
|
dialog._run_report()
|
||||||
|
|
||||||
# Should aggregate to single month
|
# Should aggregate to single month
|
||||||
assert dialog.table.rowCount() == 1
|
assert dialog.table.rowCount() == 1
|
||||||
hours_text = dialog.table.item(0, 4).text()
|
hours_text = dialog.table.item(0, 3).text()
|
||||||
|
|
||||||
assert "2.5" in hours_text or "2.50" in hours_text
|
assert "2.5" in hours_text or "2.50" in hours_text
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1940,7 +1951,10 @@ def test_time_report_dialog_stores_report_state(qtbot, fresh_db):
|
||||||
dialog.project_combo.setCurrentIndex(1)
|
dialog.project_combo.setCurrentIndex(1)
|
||||||
dialog.from_date.setDate(QDate.fromString(_today(), "yyyy-MM-dd"))
|
dialog.from_date.setDate(QDate.fromString(_today(), "yyyy-MM-dd"))
|
||||||
dialog.to_date.setDate(QDate.fromString(_today(), "yyyy-MM-dd"))
|
dialog.to_date.setDate(QDate.fromString(_today(), "yyyy-MM-dd"))
|
||||||
dialog.granularity.setCurrentIndex(1) # week
|
|
||||||
|
idx_week = dialog.granularity.findData("week")
|
||||||
|
assert idx_week != -1
|
||||||
|
dialog.granularity.setCurrentIndex(idx_week)
|
||||||
|
|
||||||
dialog._run_report()
|
dialog._run_report()
|
||||||
|
|
||||||
|
|
@ -1976,7 +1990,10 @@ def test_time_report_dialog_pdf_export_with_multiple_periods(
|
||||||
dialog.project_combo.setCurrentIndex(0)
|
dialog.project_combo.setCurrentIndex(0)
|
||||||
dialog.from_date.setDate(QDate.fromString(date1, "yyyy-MM-dd"))
|
dialog.from_date.setDate(QDate.fromString(date1, "yyyy-MM-dd"))
|
||||||
dialog.to_date.setDate(QDate.fromString(date3, "yyyy-MM-dd"))
|
dialog.to_date.setDate(QDate.fromString(date3, "yyyy-MM-dd"))
|
||||||
dialog.granularity.setCurrentIndex(0) # day
|
|
||||||
|
idx_day = dialog.granularity.findData("day")
|
||||||
|
assert idx_day != -1
|
||||||
|
dialog.granularity.setCurrentIndex(idx_day)
|
||||||
|
|
||||||
dialog._run_report()
|
dialog._run_report()
|
||||||
|
|
||||||
|
|
@ -2933,3 +2950,69 @@ def test_time_code_manager_dialog_with_focus_tab(qtbot, app, fresh_db):
|
||||||
dialog2 = TimeCodeManagerDialog(fresh_db, focus_tab="activities")
|
dialog2 = TimeCodeManagerDialog(fresh_db, focus_tab="activities")
|
||||||
qtbot.addWidget(dialog2)
|
qtbot.addWidget(dialog2)
|
||||||
assert dialog2.tabs.currentIndex() == 1
|
assert dialog2.tabs.currentIndex() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_time_report_no_grouping_returns_each_entry_and_note(fresh_db):
|
||||||
|
"""Granularity 'none' returns one row per entry and includes notes."""
|
||||||
|
proj_id = fresh_db.add_project("Project")
|
||||||
|
act_id = fresh_db.add_activity("Activity")
|
||||||
|
date = _today()
|
||||||
|
|
||||||
|
fresh_db.add_time_log(date, proj_id, act_id, 60, note="First")
|
||||||
|
fresh_db.add_time_log(date, proj_id, act_id, 30, note="Second")
|
||||||
|
|
||||||
|
report = fresh_db.time_report(proj_id, date, date, "none")
|
||||||
|
|
||||||
|
# Two separate rows, not aggregated.
|
||||||
|
assert len(report) == 2
|
||||||
|
|
||||||
|
# Each row is (period, activity_name, note, total_minutes)
|
||||||
|
periods = {r[0] for r in report}
|
||||||
|
activities = {r[1] for r in report}
|
||||||
|
notes = {r[2] for r in report}
|
||||||
|
minutes = sorted(r[3] for r in report)
|
||||||
|
|
||||||
|
assert periods == {date}
|
||||||
|
assert activities == {"Activity"}
|
||||||
|
assert notes == {"First", "Second"}
|
||||||
|
assert minutes == [30, 60]
|
||||||
|
|
||||||
|
|
||||||
|
def test_time_report_dialog_granularity_none_shows_each_entry_and_notes(
|
||||||
|
qtbot, fresh_db
|
||||||
|
):
|
||||||
|
"""'Don't group' granularity shows one row per log entry and includes notes."""
|
||||||
|
strings.load_strings("en")
|
||||||
|
proj_id = fresh_db.add_project("Project")
|
||||||
|
act_id = fresh_db.add_activity("Activity")
|
||||||
|
date = _today()
|
||||||
|
|
||||||
|
fresh_db.add_time_log(date, proj_id, act_id, 60, note="First")
|
||||||
|
fresh_db.add_time_log(date, proj_id, act_id, 30, note="Second")
|
||||||
|
|
||||||
|
dialog = TimeReportDialog(fresh_db)
|
||||||
|
qtbot.addWidget(dialog)
|
||||||
|
|
||||||
|
# Select the concrete project (index 0 is "All projects")
|
||||||
|
dialog.project_combo.setCurrentIndex(1)
|
||||||
|
dialog.from_date.setDate(QDate.fromString(date, "yyyy-MM-dd"))
|
||||||
|
dialog.to_date.setDate(QDate.fromString(date, "yyyy-MM-dd"))
|
||||||
|
|
||||||
|
idx_none = dialog.granularity.findData("none")
|
||||||
|
assert idx_none != -1
|
||||||
|
dialog.granularity.setCurrentIndex(idx_none)
|
||||||
|
|
||||||
|
dialog._run_report()
|
||||||
|
|
||||||
|
# Two rows, not aggregated
|
||||||
|
assert dialog.table.rowCount() == 2
|
||||||
|
|
||||||
|
# Notes in column 3
|
||||||
|
notes = {dialog.table.item(row, 3).text() for row in range(dialog.table.rowCount())}
|
||||||
|
assert "First" in notes
|
||||||
|
assert "Second" in notes
|
||||||
|
|
||||||
|
# Hours in last column (index 4) when not grouped
|
||||||
|
hours = [dialog.table.item(row, 4).text() for row in range(dialog.table.rowCount())]
|
||||||
|
assert any("1.00" in h or "1.0" in h for h in hours)
|
||||||
|
assert any("0.50" in h or "0.5" in h for h in hours)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue