diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59d65adb..13101f6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## 3.7.8.dev
+* Fix: time-range filter on a VTODO having DTSTART/DUE and also CREATED/COMPLETED used the CREATED->COMPLETED duration instead of the DTSTART->DUE one, so completed tasks were missing from (or wrongly returned by) calendar-query REPORT results
* Fix: sharing/proppatch: reject in case of write-access but 'p' is in permissions
* Fix: sharing/by-map: catch collection path without trailing / (supporting "pimsync")
* Add: [report] max_expand_occurrence option to separate from max_freebusy_occurrence
diff --git a/radicale/item/filter.py b/radicale/item/filter.py
index 74e571ca..789cafe3 100644
--- a/radicale/item/filter.py
+++ b/radicale/item/filter.py
@@ -442,7 +442,10 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
completed = date_to_datetime(completed.value)
if created is not None:
created = date_to_datetime(created.value)
- original_duration = (completed - created).total_seconds()
+ # NOTE: kept separate from "original_duration", otherwise a
+ # VTODO with DTSTART+DUE and also CREATED+COMPLETED would
+ # lose its DTSTART->DUE duration (see line 2 below)
+ completed_duration = (completed - created).total_seconds()
elif created is not None:
created = date_to_datetime(created.value)
@@ -500,7 +503,7 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
elif completed is not None and created is not None:
# Line 5
completed = reference_date + timedelta(
- seconds=original_duration)
+ seconds=completed_duration)
if (range_fn(reference_date - SECOND,
reference_date + SECOND,
is_recurrence) or
diff --git a/radicale/tests/static/todo10.ics b/radicale/tests/static/todo10.ics
new file mode 100644
index 00000000..5acbde84
--- /dev/null
+++ b/radicale/tests/static/todo10.ics
@@ -0,0 +1,13 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Radicale//NONSGML Radicale Server//EN
+BEGIN:VTODO
+UID:todo10
+SUMMARY:todo10
+DTSTART:20130901T180000Z
+DUE:20130903T180000Z
+CREATED:20130901T170000Z
+COMPLETED:20130901T173000Z
+STATUS:COMPLETED
+END:VTODO
+END:VCALENDAR
diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py
index 506d0871..7c2c2902 100644
--- a/radicale/tests/test_base.py
+++ b/radicale/tests/test_base.py
@@ -2083,6 +2083,27 @@ permissions: RrWw""")
"""], "todo", items=range(1, 9))
assert "/calendar.ics/todo7.ics" in answer
+ def test_time_range_filter_todos_dtstart_due_completed(self) -> None:
+ """Report request with time-range filter on a completed todo which
+ also has DTSTART and DUE (rfc4791-9.9: DTSTART/DUE take
+ precedence over CREATED/COMPLETED)."""
+ # inside DTSTART..DUE, but outside CREATED..COMPLETED
+ answer = self._test_filter(["""\
+
+
+
+
+"""], "todo", items=(10,))
+ assert "/calendar.ics/todo10.ics" in answer
+ # entirely before DTSTART and CREATED
+ answer = self._test_filter(["""\
+
+
+
+
+"""], "todo", items=(10,))
+ assert "/calendar.ics/todo10.ics" not in answer
+
def test_time_range_filter_events_valarm(self) -> None:
"""Report request with time-range filter on events having absolute VALARM."""
answer = self._test_filter(["""\