Fix manual time entry timezone (Issue #471)

Manual time entries were interpreted in the application timezone instead of the user's timezone, so users in a different timezone (e.g. GMT+1) saw times adjusted to the server timezone (e.g. GMT+0) on the calendar.

- Add parse_user_local_datetime() in timezone utils to parse form date/time as the user's local time and return naive datetime in app timezone for storage.
- Use parse_user_local_datetime() in the manual entry form so submitted times are stored correctly and display at the right slot in the calendar.

Calendar events and drag-created entries were already correct; only the manual entry form path is changed. Other callers of parse_local_datetime are unchanged.
This commit is contained in:
Dries Peeters
2026-01-30 15:59:25 +01:00
parent f117a0cb58
commit 940e1c8170
2 changed files with 24 additions and 4 deletions
+4 -4
View File
@@ -3,7 +3,7 @@ from flask_babel import gettext as _
from flask_login import login_required, current_user
from app import db, socketio, log_event, track_event
from app.models import User, Project, TimeEntry, Task, Settings, Activity, Client
from app.utils.timezone import parse_local_datetime, utc_to_local
from app.utils.timezone import parse_local_datetime, parse_user_local_datetime, utc_to_local
from datetime import datetime, timedelta
import json
from app.utils.db import safe_commit
@@ -1068,10 +1068,10 @@ def manual_entry():
prefill_end_time=end_time,
)
# Parse datetime with timezone awareness
# Parse datetime: treat form input as user's local time, store in app timezone
try:
start_time_parsed = parse_local_datetime(start_date, start_time)
end_time_parsed = parse_local_datetime(end_date, end_time)
start_time_parsed = parse_user_local_datetime(start_date, start_time, current_user)
end_time_parsed = parse_user_local_datetime(end_date, end_time, current_user)
except ValueError:
flash(_("Invalid date/time format"), "error")
return render_template(
+20
View File
@@ -222,6 +222,26 @@ def parse_local_datetime(date_str, time_str):
raise ValueError(f"Invalid date/time format: {e}")
def parse_user_local_datetime(date_str, time_str, user=None):
"""Parse date and time strings as user's local time; return naive datetime in app timezone for storage.
Use this for manual time entry forms where the user enters a time in their local timezone.
When user has no timezone set, falls back to app timezone (same as parse_local_datetime input semantics).
"""
try:
datetime_str = f"{date_str} {time_str}"
naive_dt = datetime.strptime(datetime_str, "%Y-%m-%d %H:%M")
# Treat input as user's timezone (or app timezone if no user / no user TZ)
user_tz = get_timezone_for_user(user)
app_tz = get_timezone_obj()
localized_in_user_tz = _localize_with_timezone(naive_dt, user_tz)
in_app_tz = localized_in_user_tz.astimezone(app_tz)
return in_app_tz.replace(tzinfo=None)
except ValueError as e:
raise ValueError(f"Invalid date/time format: {e}")
def format_local_datetime(utc_dt, format_str="%Y-%m-%d %H:%M"):
"""Format UTC datetime in local application timezone."""
if utc_dt is None: