diff --git a/todoman/model.py b/todoman/model.py
index 12f7a33..cff1434 100644
--- a/todoman/model.py
+++ b/todoman/model.py
@@ -99,6 +99,7 @@ class Todo:
         self.dtstamp = now
         self.due = None
         self.id = None
+        self.last_modified = None
         self.location = ''
         self.percent_complete = 0
         self.priority = 0
@@ -158,6 +159,7 @@ class Todo:
         'dtstamp',
         'start',
         'due',
+        'last_modified',
     ]
     ALL_SUPPORTED_FIELDS = (
         DATETIME_FIELDS +
@@ -234,6 +236,7 @@ class VtodoWritter:
         'priority': 'priority',
         'status': 'status',
         'created_at': 'created',
+        'last_modified': 'last-modified',
     }
 
     def __init__(self, todo):
@@ -249,9 +252,6 @@ class VtodoWritter:
         '''
         if isinstance(dt, date) and not isinstance(dt, datetime):
             dt = datetime(dt.year, dt.month, dt.day)
-        # XXX: Can we actually get times from the UI?
-        elif isinstance(dt, time):
-            dt = datetime.combine(date.today(), dt)
 
         if not dt.tzinfo:
             dt = dt.replace(tzinfo=LOCAL_TIMEZONE)
@@ -347,7 +347,7 @@ class Cache:
     may be used for filtering/sorting.
     """
 
-    SCHEMA_VERSION = 3
+    SCHEMA_VERSION = 4
 
     def __init__(self, path):
         self.cache_path = str(path)
@@ -429,6 +429,8 @@ class Cache:
                 "description" TEXT,
                 "location" TEXT,
                 "categories" TEXT,
+                "sequence" INTEGER,
+                "last_modified" INTEGER,
 
                 FOREIGN KEY(file_path) REFERENCES files(path) ON DELETE CASCADE
             );
@@ -518,8 +520,10 @@ class Cache:
                 status,
                 description,
                 location,
-                categories
-            ) VALUES ({}?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+                categories,
+                sequence,
+                last_modified
+            ) VALUES ({}?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
             '''
 
         due = self._serialize_datetime(todo, 'due')
@@ -543,6 +547,8 @@ class Cache:
             todo.get('description', None),
             todo.get('location', None),
             todo.get('categories', None),
+            todo.get('sequence', 1),
+            self._serialize_datetime(todo, 'last-modified'),
         )
 
         if id:
@@ -704,6 +710,8 @@ class Cache:
         todo.status = row['status']
         todo.description = row['description']
         todo.location = row['location']
+        todo.sequence = row['sequence']
+        todo.last_modified = row['last_modified']
         todo.list = self.lists_map[row['list_name']]
         todo.filename = os.path.basename(row['path'])
         return todo
@@ -906,6 +914,8 @@ class Database:
 
     def save(self, todo):
         todo.sequence += 1
+        todo.last_modified = datetime.now(LOCAL_TIMEZONE)
+
         vtodo = VtodoWritter(todo).write()
 
         self.cache.expire_file(todo.path)

Increment sequence number upon edition
This issue was moved from the issue tracker at GitLab.

---

Increment the [sequence number](https://tools.ietf.org/html/rfc5545#section-3.8.7.4) every time the file is modified/save.

* [x] `SEQUENCE`
* [x] `LAST-MODIFIED`
diff --git a/tests/test_backend.py b/tests/test_backend.py
index 56a3dd9..7a4a771 100644
--- a/tests/test_backend.py
+++ b/tests/test_backend.py
@@ -3,6 +3,7 @@ from datetime import datetime
 import pytest
 import pytz
 from dateutil.tz import tzlocal
+from freezegun import freeze_time
 
 from todoman.model import Todo, VtodoWritter
 
@@ -57,3 +58,21 @@ def test_vtodo_serialization(todo_factory):
     assert vtodo.get('priority') == 7
     assert vtodo.decoded('due') == datetime(3000, 3, 21, tzinfo=tzlocal())
     assert str(vtodo.get('status')) == 'IN-PROCESS'
+
+
+@freeze_time('2017-04-04 20:11:57')
+def test_update_last_modified(todo_factory, todos, tmpdir):
+    todo = todo_factory()
+    assert todo.last_modified == datetime.now(tzlocal())
+
+
+def test_sequence_increment(default_database, todo_factory, todos):
+    todo = todo_factory()
+    assert todo.sequence == 1
+
+    default_database.save(todo)
+    assert todo.sequence == 2
+
+    # Relaod (and check the caching flow for the sequence)
+    todo = next(todos())
+    assert todo.sequence == 2
diff --git a/tests/test_model.py b/tests/test_model.py
index 7ac59b2..fd41f52 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -1,6 +1,5 @@
 from datetime import datetime
 
-import icalendar
 import pytest
 import pytz
 from dateutil.tz import tzlocal
@@ -72,29 +71,6 @@ def test_change_paths(tmpdir, create):
     assert not list(db.todos())
 
 
-def test_sequence_increment(tmpdir, default_database):
-    todo = Todo(new=True, list=next(default_database.lists()))
-    default_database.save(todo)
-
-    with open(todo.path) as f:
-        cal = icalendar.Calendar.from_ical(f.read())
-    sequence, = [component.get("SEQUENCE", 0)
-                 for component in cal.subcomponents
-                 if component.name == "VTODO"]
-
-    assert sequence == 1
-
-    default_database.save(todo)
-
-    with open(todo.path) as f:
-        cal = icalendar.Calendar.from_ical(f.read())
-    sequence, = [component.get("SEQUENCE", 0)
-                 for component in cal.subcomponents
-                 if component.name == "VTODO"]
-
-    assert sequence == 2
-
-
 def test_list_displayname(tmpdir):
     tmpdir.join('default').mkdir()
     with tmpdir.join('default').join('displayname').open('w') as f:

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.7
requirements.txt requirements-dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
atomicwrites==1.4.1
attrs==24.2.0
backports.zoneinfo==0.2.1
certifi @ file:///croot/certifi_1671487769961/work/certifi
click==8.1.8
click-log==0.4.0
configobj==5.0.9
coverage==7.2.7
exceptiongroup==1.2.2
flake8==5.0.4
flake8-import-order==0.18.2
freezegun==1.5.1
humanize==4.6.0
hypothesis==6.79.4
icalendar==5.0.13
importlib-metadata==4.2.0
iniconfig==2.0.0
mccabe==0.7.0
packaging==24.0
parsedatetime==2.6
pluggy==1.2.0
pycodestyle==2.9.1
pyflakes==2.5.0
pytest==7.4.4
pytest-cov==4.1.0
python-dateutil==2.9.0.post0
pytz==2025.2
pyxdg==0.28
six==1.17.0
sortedcontainers==2.4.0
tabulate==0.9.0
-e git+https://github.com/pimutils/todoman.git@bbbefc26fb7b404e828e0a43df77150028cf359e#egg=todoman
tomli==2.0.1
typing_extensions==4.7.1
urwid==2.6.16
wcwidth==0.2.13
zipp==3.15.0

name: todoman
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - atomicwrites==1.4.1
      - attrs==24.2.0
      - backports-zoneinfo==0.2.1
      - click==8.1.8
      - click-log==0.4.0
      - configobj==5.0.9
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - flake8==5.0.4
      - flake8-import-order==0.18.2
      - freezegun==1.5.1
      - humanize==4.6.0
      - hypothesis==6.79.4
      - icalendar==5.0.13
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - mccabe==0.7.0
      - packaging==24.0
      - parsedatetime==2.6
      - pluggy==1.2.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyxdg==0.28
      - six==1.17.0
      - sortedcontainers==2.4.0
      - tabulate==0.9.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urwid==2.6.16
      - wcwidth==0.2.13
      - zipp==3.15.0
prefix: /opt/conda/envs/todoman

tests/test_backend.py::test_update_last_modified tests/test_backend.py::test_sequence_increment
tests/test_backend.py::test_vtodo_serialization
tests/test_backend.py::test_serialize_created_at tests/test_backend.py::test_serialize_dtstart tests/test_backend.py::test_serializer_raises tests/test_backend.py::test_supported_fields_are_serializeable tests/test_model.py::test_querying tests/test_model.py::test_retain_tz tests/test_model.py::test_change_paths tests/test_model.py::test_list_displayname tests/test_model.py::test_list_colour tests/test_model.py::test_list_no_colour tests/test_model.py::test_database_priority_sorting tests/test_model.py::test_retain_unknown_fields tests/test_model.py::test_todo_setters tests/test_model.py::test_is_completed tests/test_model.py::test_todo_filename_absolute_path tests/test_model.py::test_list_equality tests/test_model.py::test_clone tests/test_model.py::test_todos_startable tests/test_model.py::test_filename_uid_colision tests/test_model.py::test_hide_cancelled tests/test_model.py::test_illegal_start_suppression tests/test_model.py::test_default_status
todoman/model.py
todoman/model.py