Files
TimeTracker/docs/TIME_ROUNDING_PREFERENCES.md
Dries Peeters 48ec29e096 feat: Add per-user time rounding preferences
Implement comprehensive time rounding preferences that allow each user to
configure how their time entries are rounded when stopping timers.

Features:
- Per-user rounding settings (independent from global config)
- Multiple rounding intervals: 1, 5, 10, 15, 30, 60 minutes
- Three rounding methods: nearest, up (ceiling), down (floor)
- Enable/disable toggle for flexible time tracking
- Real-time preview showing rounding examples
- Backward compatible with existing global rounding settings

Database Changes:
- Add migration 027 with three new user columns:
  * time_rounding_enabled (Boolean, default: true)
  * time_rounding_minutes (Integer, default: 1)
  * time_rounding_method (String, default: 'nearest')

Implementation:
- Update User model with rounding preference fields
- Modify TimeEntry.calculate_duration() to use per-user rounding
- Create app/utils/time_rounding.py with core rounding logic
- Update user settings route and template with rounding UI
- Add comprehensive unit, model, and smoke tests (50+ test cases)

UI/UX:
- Add "Time Rounding Preferences" section to user settings page
- Interactive controls with live example visualization
- Descriptive help text and method explanations
- Fix navigation: Settings link now correctly points to user.settings
- Fix CSRF token in settings form

Documentation:
- Add comprehensive user guide (docs/TIME_ROUNDING_PREFERENCES.md)
- Include API documentation and usage examples
- Provide troubleshooting guide and best practices
- Add deployment instructions for migration

Testing:
- Unit tests for rounding logic (tests/test_time_rounding.py)
- Model integration tests (tests/test_time_rounding_models.py)
- End-to-end smoke tests (tests/test_time_rounding_smoke.py)

Fixes:
- Correct settings navigation link in user dropdown menu
- Fix CSRF token format in user settings template

This feature enables flexible billing practices, supports different client
requirements, and maintains exact time tracking when needed.
2025-10-24 09:36:03 +02:00

9.5 KiB

Time Rounding Preferences - Per-User Settings

Overview

The Time Rounding Preferences feature allows each user to configure how their time entries are rounded when they stop timers. This provides flexibility for different billing practices and time tracking requirements while maintaining accurate time records.

Key Features

  • Per-User Configuration: Each user can set their own rounding preferences independently
  • Multiple Rounding Intervals: Support for 1, 5, 10, 15, 30, and 60-minute intervals
  • Three Rounding Methods:
    • Nearest: Round to the closest interval (standard rounding)
    • Up: Always round up to the next interval (ceiling)
    • Down: Always round down to the previous interval (floor)
  • Enable/Disable Toggle: Users can disable rounding to track exact time
  • Real-time Preview: Visual examples show how rounding will be applied

User Guide

Accessing Rounding Settings

  1. Navigate to Settings from the user menu
  2. Scroll to the Time Rounding Preferences section
  3. Configure your preferences:
    • Toggle Enable Time Rounding on/off
    • Select your preferred Rounding Interval
    • Choose your Rounding Method
  4. Click Save Settings to apply changes

Understanding Rounding Methods

Round to Nearest (Default)

Standard mathematical rounding to the closest interval.

Example with 15-minute intervals:

  • 7 minutes → 0 minutes
  • 8 minutes → 15 minutes
  • 62 minutes → 60 minutes
  • 68 minutes → 75 minutes

Always Round Up

Always rounds up to the next interval, ensuring you never under-bill.

Example with 15-minute intervals:

  • 1 minute → 15 minutes
  • 61 minutes → 75 minutes
  • 60 minutes → 60 minutes (exact match)

Always Round Down

Always rounds down to the previous interval, ensuring conservative billing.

Example with 15-minute intervals:

  • 14 minutes → 0 minutes
  • 74 minutes → 60 minutes
  • 75 minutes → 75 minutes (exact match)

Choosing the Right Settings

For Freelancers/Contractors:

  • Use 15-minute intervals with Round to Nearest for balanced billing
  • Use Round Up if client agreements favor rounding up
  • Use 5 or 10 minutes for more granular tracking

For Internal Time Tracking:

  • Use No rounding (1 minute) for exact time tracking
  • Use 15 or 30 minutes for simplified reporting

For Project-Based Billing:

  • Use 30 or 60 minutes for project-level granularity
  • Use Round Down for conservative estimates

Technical Details

Database Schema

The following fields are added to the users table:

time_rounding_enabled BOOLEAN DEFAULT 1 NOT NULL
time_rounding_minutes INTEGER DEFAULT 1 NOT NULL
time_rounding_method VARCHAR(10) DEFAULT 'nearest' NOT NULL

Default Values

For new and existing users:

  • Enabled: True (rounding is enabled by default)
  • Minutes: 1 (no rounding, exact time)
  • Method: 'nearest' (standard rounding)

How Rounding is Applied

  1. Timer Start: When a user starts a timer, no rounding is applied
  2. Timer Stop: When a user stops a timer:
    • Calculate raw duration (end time - start time)
    • Apply user's rounding preferences
    • Store rounded duration in duration_seconds field
  3. Manual Entries: Rounding is applied when creating/editing manual entries

Backward Compatibility

The feature is fully backward compatible:

  • If user preferences don't exist, the system falls back to the global ROUNDING_MINUTES config setting
  • Existing time entries are not retroactively rounded
  • Users without the new fields will use global rounding settings

API Integration

Get User Rounding Settings

from app.utils.time_rounding import get_user_rounding_settings

settings = get_user_rounding_settings(user)
# Returns: {'enabled': True, 'minutes': 15, 'method': 'nearest'}

Apply Rounding to Duration

from app.utils.time_rounding import apply_user_rounding

raw_seconds = 3720  # 62 minutes
rounded_seconds = apply_user_rounding(raw_seconds, user)
# Returns: 3600 (60 minutes) with 15-min nearest rounding

Manual Rounding

from app.utils.time_rounding import round_time_duration

rounded = round_time_duration(
    duration_seconds=3720,  # 62 minutes
    rounding_minutes=15,
    rounding_method='up'
)
# Returns: 4500 (75 minutes)

Migration Guide

Applying the Migration

Run the Alembic migration to add the new fields:

# Using Alembic
alembic upgrade head

# Or using the migration script
python migrations/manage_migrations.py upgrade

Migration Details

  • Migration File: migrations/versions/027_add_user_time_rounding_preferences.py
  • Adds: Three new columns to the users table
  • Safe: Non-destructive, adds columns with default values
  • Rollback: Supported via downgrade function

Verifying Migration

from app.models import User
from app import db

# Check if fields exist
user = User.query.first()
assert hasattr(user, 'time_rounding_enabled')
assert hasattr(user, 'time_rounding_minutes')
assert hasattr(user, 'time_rounding_method')

# Check default values
assert user.time_rounding_enabled == True
assert user.time_rounding_minutes == 1
assert user.time_rounding_method == 'nearest'

Configuration

Available Rounding Intervals

The following intervals are supported:

  • 1 - No rounding (exact time)
  • 5 - 5 minutes
  • 10 - 10 minutes
  • 15 - 15 minutes
  • 30 - 30 minutes (half hour)
  • 60 - 60 minutes (1 hour)

Available Rounding Methods

Three methods are supported:

  • 'nearest' - Round to nearest interval
  • 'up' - Always round up (ceiling)
  • 'down' - Always round down (floor)

Global Fallback Setting

If per-user rounding is not configured, the system uses the global setting:

# In app/config.py
ROUNDING_MINUTES = int(os.environ.get('ROUNDING_MINUTES', 1))

Testing

Running Tests

# Run all time rounding tests
pytest tests/test_time_rounding*.py -v

# Run specific test suites
pytest tests/test_time_rounding.py -v  # Unit tests
pytest tests/test_time_rounding_models.py -v  # Model integration tests
pytest tests/test_time_rounding_smoke.py -v  # Smoke tests

Test Coverage

The feature includes:

  • Unit Tests: Core rounding logic (50+ test cases)
  • Model Tests: Database integration and TimeEntry model
  • Smoke Tests: End-to-end workflows and edge cases

Examples

Example 1: Freelancer with 15-Minute Billing

# User settings
user.time_rounding_enabled = True
user.time_rounding_minutes = 15
user.time_rounding_method = 'nearest'

# Time entry: 62 minutes
# Result: 60 minutes (rounded to nearest 15-min interval)

Example 2: Contractor with Round-Up Policy

# User settings
user.time_rounding_enabled = True
user.time_rounding_minutes = 15
user.time_rounding_method = 'up'

# Time entry: 61 minutes
# Result: 75 minutes (rounded up to next 15-min interval)

Example 3: Exact Time Tracking

# User settings
user.time_rounding_enabled = False

# Time entry: 62 minutes 37 seconds
# Result: 62 minutes 37 seconds (3757 seconds, exact)

Example 4: Conservative Billing

# User settings
user.time_rounding_enabled = True
user.time_rounding_minutes = 30
user.time_rounding_method = 'down'

# Time entry: 62 minutes
# Result: 60 minutes (rounded down to previous 30-min interval)

Troubleshooting

Rounding Not Applied

Issue: Time entries are not being rounded despite settings being enabled.

Solutions:

  1. Verify rounding is enabled: Check user.time_rounding_enabled == True
  2. Check rounding interval: Ensure user.time_rounding_minutes > 1
  3. Verify migration was applied: Check if columns exist in database
  4. Clear cache and restart application

Unexpected Rounding Results

Issue: Durations are rounded differently than expected.

Solutions:

  1. Verify rounding method setting (nearest/up/down)
  2. Check the actual rounding interval (minutes value)
  3. Test with example calculations using the utility functions
  4. Review the rounding method documentation

Migration Fails

Issue: Alembic migration fails to apply.

Solutions:

  1. Check database permissions
  2. Verify no conflicting migrations
  3. Run alembic current to check migration state
  4. Try manual column addition as fallback
  5. Check logs for specific error messages

Best Practices

  1. Choose Appropriate Intervals: Match your rounding to billing agreements
  2. Document Your Choice: Note why you chose specific rounding settings
  3. Test Before Production: Verify rounding behavior with test entries
  4. Communicate with Clients: Ensure clients understand your rounding policy
  5. Review Regularly: Periodically review if rounding settings still make sense
  6. Keep Records: Document any changes to rounding preferences

Future Enhancements

Potential improvements for future versions:

  • Project-specific rounding overrides
  • Time-of-day based rounding rules
  • Client-specific rounding preferences
  • Rounding reports and analytics
  • Bulk update of historical entries with new rounding

Support

For issues or questions:

  1. Check this documentation first
  2. Review test files for usage examples
  3. Check the codebase in app/utils/time_rounding.py
  4. Open an issue on the project repository

Changelog

Version 1.0 (2025-10-24)

  • Initial implementation of per-user time rounding preferences
  • Support for 6 rounding intervals (1, 5, 10, 15, 30, 60 minutes)
  • Support for 3 rounding methods (nearest, up, down)
  • UI integration in user settings page
  • Comprehensive test coverage
  • Full backward compatibility with global rounding settings