-
Notifications
You must be signed in to change notification settings - Fork 2
Home
- Project Overview
- Architecture
- Project Structure
- Core Components
- Data Flow
- Database Schema
- Synchronization System
- Background Daemon
- QML-Python Bridge
- Notification System
- Building and Running
- Debugging Guide
- Files to Remove (Development/Testing Only)
- Common Issues & Solutions
Time Management (UBTMS) is a time tracking and project management application for Ubuntu Touch devices. It integrates with Odoo ERP for syncing projects, tasks, timesheets, and activities.
- Multi-account Odoo synchronization
- Offline-first architecture with local SQLite database
- Background sync daemon with push notifications
- Timesheet tracking with timer functionality
- Eisenhower matrix (quadrant-based) task prioritization
- Project and task management
- Activity scheduling and tracking
- Draft auto-save system for crash recovery
| Layer | Technology |
|---|---|
| UI Framework | QML (Qt Quick) with Lomiri Components |
| Business Logic | JavaScript (QML models) + Python (backend) |
| Database | SQLite (via Qt LocalStorage) |
| Odoo Integration | Python XML-RPC |
| Notifications | C++ QML Plugin + DBus |
| Build System | Clickable + CMake |
┌─────────────────────────────────────────────────────────────────────┐
│ USER INTERFACE │
│ QML Pages (Dashboard, Tasks, etc.) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ MODELS LAYER │
│ JavaScript Modules (models/*.js) │
│ project.js │ task.js │ timesheet.js │ activity.js │ accounts.js │
└─────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ LOCAL DATABASE │ │ BACKEND BRIDGE │
│ SQLite (LocalStorage) │ │ (pyotherside → Python) │
│ dbinit.js │ database.js │ │ BackendBridge.qml │
└───────────────────────────────┘ └───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PYTHON BACKEND │
│ backend.py │ sync_from_odoo.py │ sync_to_odoo.py │
│ odoo_client.py │ common.py │ config.py │
└─────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ ODOO SERVER │ │ BACKGROUND DAEMON │
│ (XML-RPC API) │ │ daemon.py (DBus/GLib) │
└───────────────────────────────┘ └───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ NOTIFICATION SYSTEM │
│ C++ NotificationHelper │ Lomiri Postal │ freedesktop │
└─────────────────────────────────────────────────────────────────────┘
timemanagement/
├── assets/ # App icons and images
├── models/ # JavaScript business logic modules
│ ├── Main.js # Dashboard statistics and utilities
│ ├── database.js # Database helper functions
│ ├── dbinit.js # Database schema initialization
│ ├── project.js # Project CRUD operations
│ ├── task.js # Task CRUD with multi-assignee support
│ ├── timesheet.js # Timesheet management
│ ├── activity.js # Activity/scheduling operations
│ ├── accounts.js # Account management
│ ├── timer_service.js # Global timer singleton
│ ├── notifications.js # In-app notification handling
│ ├── draft_manager.js # Form auto-save/recovery
│ ├── utils.js # Common utilities
│ ├── constants.js # App-wide constants
│ └── global.js # Global state management
│
├── qml/ # QML UI files
│ ├── TSApp.qml # Main application entry point
│ ├── Menu.qml # Navigation menu
│ ├── Dashboard.qml # Main dashboard
│ ├── Projects.qml # Projects list
│ ├── Project_Page.qml # Project detail/edit
│ ├── Tasks.qml # Tasks list
│ ├── Task_Page.qml # Task detail/edit
│ ├── Timesheet.qml # Timesheets list
│ ├── Timesheet_Page.qml # Timesheet detail/edit
│ ├── Activities.qml # Activities list
│ ├── Activity_Page.qml # Activity detail/edit
│ ├── Account_Page.qml # Account configuration
│ ├── Settings_Page.qml # App settings
│ ├── Charts*.qml # Various chart views
│ └── components/ # Reusable QML components
│ ├── BackendBridge.qml # Python integration bridge
│ ├── GlobalTimerWidget.qml # Floating timer widget
│ ├── NotificationBell.qml # Notification UI
│ ├── AccountSelector.qml # Account switcher
│ ├── FormDraftHandler.qml # Draft auto-save handler
│ ├── TaskList.qml # Task list component
│ ├── ProjectList.qml # Project list component
│ └── ... (50+ components)
│
├── src/ # Python backend
│ ├── backend.py # Main backend API for QML
│ ├── odoo_client.py # Odoo XML-RPC client
│ ├── sync_from_odoo.py # Sync: Odoo → Local DB
│ ├── sync_to_odoo.py # Sync: Local DB → Odoo
│ ├── daemon.py # Background sync daemon
│ ├── daemon_bootstrap.py # Daemon systemd setup
│ ├── bus.py # QML event bus (pyotherside)
│ ├── config.py # Configuration management
│ ├── common.py # Shared utilities
│ ├── logger.py # Logging setup
│ └── field_config.json # Odoo ↔ SQLite field mapping
│
├── qml-notify-module/ # C++ notification plugin
│ ├── NotificationHelper.h
│ ├── NotificationHelper.cpp
│ ├── plugin.cpp
│ └── CMakeLists.txt
│
├── po/ # Translations
├── manifest.json.in # Click package manifest
├── clickable.yaml # Build configuration
├── CMakeLists.txt # CMake build file
└── start-daemon.sh # Daemon startup script
The user interface is built with QML using Lomiri Components (Ubuntu Touch's UI toolkit).
Key Files:
-
TSApp.qml: Main application entry point. Contains:
-
AdaptivePageLayoutfor responsive multi-column layout - Deep link handling via
UriHandler - Global timer widget integration
- Account switching logic
-
-
Menu.qml: Navigation sidebar with links to all main sections
-
Dashboard.qml: Home screen with:
- Quadrant summary (Eisenhower matrix)
- Project pie charts
- Notification bell
- Quick actions
Business logic is implemented in JavaScript modules that QML imports.
Key Patterns:
// Import pattern
.import QtQuick.LocalStorage 2.7 as Sql
.import "database.js" as DBCommon
// Database access pattern
function getProjectDetails(project_id) {
var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE);
var result = {};
db.transaction(function(tx) {
var rs = tx.executeSql('SELECT * FROM project_project_app WHERE id = ?', [project_id]);
if (rs.rows.length > 0) {
result = DBCommon.rowToObject(rs.rows.item(0));
}
});
return result;
}Database Schema Initialization (dbinit.js):
- Called on app startup
- Creates all required tables
- Handles schema migrations (adds missing columns)
- Creates default "Local Account" for offline use
Draft Manager (draft_manager.js):
- Auto-saves form state every few seconds
- Detects unsaved changes
- Provides crash recovery for in-progress edits
The Python backend handles Odoo integration via pyotherside.
Key Modules:
| Module | Purpose |
|---|---|
backend.py |
Main API exposed to QML; sync, login, file operations |
odoo_client.py |
XML-RPC client for Odoo with timeout handling |
sync_from_odoo.py |
Downloads data from Odoo to local SQLite |
sync_to_odoo.py |
Uploads local changes to Odoo |
common.py |
Thread-safe SQL execution, utilities |
config.py |
Account retrieval from local DB |
bus.py |
Event system for Python→QML communication |
logger.py |
Logging with file and memory handlers |
Field Mapping (field_config.json):
Maps Odoo field names to local SQLite column names:
{
"project.project": {
"name": "name",
"date_start": "planned_start_date",
"allocated_hours": "allocated_hours",
"id": "odoo_record_id"
}
}A persistent background service that:
- Runs even when the app is closed
- Syncs with Odoo periodically (every 60 seconds)
- Sends push notifications for new tasks/activities
- Survives device sleep using wakelocks
Key Features:
- DBus integration for system notifications
- Wakelock acquisition via
com.lomiri.Repowerd - OOM killer protection (lowered oom_score_adj)
- Sleep/wake handling via logind signals
- PID file and heartbeat for health monitoring
A C++ QML plugin that provides:
-
showNotificationMessage(title, message)- Display system notification -
updateCount(count)- Update app badge count -
startDaemon()- Launch background daemon -
isDaemonHealthy()- Check daemon status
┌────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ Task_Page.qml │────▶│ task.js │────▶│ SQLite Database │
│ (User Input) │ │ saveOrUpdateTask│ │ status='created'│
└────────────────┘ └─────────────────┘ └──────────────────┘
- User fills form in
Task_Page.qml - Form data passed to
task.js::saveOrUpdateTask() - Record inserted with
status = 'created'(pending sync)
┌─────────────────┐ ┌──────────────────┐ ┌───────────────┐
│ BackendBridge │────▶│ backend.py │────▶│ Odoo Server │
│ call("sync") │ │ sync_to_odoo.py │ │ (XML-RPC) │
└─────────────────┘ └──────────────────┘ └───────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ QML receives │◀────│ bus.send() │
│ progress events│ │ sync_progress │
└─────────────────┘ └──────────────────┘
- QML calls
backend_bridge.call("backend.sync_background", [db_path, account_id]) - Python reads records with
status IN ('created', 'updated') - For each record:
- If
odoo_record_id IS NULL: Create in Odoo - Else: Update in Odoo
- If
- On success: Clear local
statusfield - Progress events sent to QML via
bus.send()
┌───────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Odoo Server │────▶│ sync_from_odoo.py│────▶│ SQLite Database │
│ search_read │ │ insert_record() │ │ INSERT/REPLACE │
└───────────────┘ └──────────────────┘ └─────────────────┘
- Fetch records modified since last sync (
write_date > last_sync) - Map Odoo fields to SQLite columns using
field_config.json - Insert or replace records in local database
- Preserve local
statusif record has pending changes
All tables are created in models/dbinit.js. Key tables:
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
link TEXT NOT NULL, -- Odoo server URL
database TEXT NOT NULL, -- Odoo database name
username TEXT NOT NULL,
api_key TEXT,
is_default INTEGER DEFAULT 0
);CREATE TABLE project_project_app (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
account_id INTEGER, -- Links to users.id
parent_id INTEGER, -- For subprojects
planned_start_date DATE,
planned_end_date DATE,
allocated_hours FLOAT,
favorites INTEGER,
description TEXT,
status TEXT DEFAULT "", -- 'created', 'updated', 'deleted'
odoo_record_id INTEGER, -- Odoo's ID
UNIQUE (odoo_record_id, account_id)
);CREATE TABLE project_task_app (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
account_id INTEGER,
project_id INTEGER, -- odoo_record_id of project
parent_id INTEGER, -- For subtasks
user_id TEXT, -- Comma-separated assignee IDs
priority TEXT,
deadline DATE,
state INTEGER, -- Stage ID
status TEXT DEFAULT "",
odoo_record_id INTEGER,
UNIQUE (odoo_record_id, account_id)
);CREATE TABLE account_analytic_line_app (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
account_id INTEGER,
project_id INTEGER,
task_id INTEGER,
user_id INTEGER,
unit_amount FLOAT, -- Hours (decimal)
record_date DATE,
quadrant_id INTEGER, -- Eisenhower quadrant (1-4)
timer_type TEXT, -- 'manual' or 'timer'
status TEXT DEFAULT "",
odoo_record_id INTEGER
);CREATE TABLE mail_activity_app (
id INTEGER PRIMARY KEY AUTOINCREMENT,
summary TEXT,
account_id INTEGER,
activity_type_id INTEGER,
due_date DATE,
user_id INTEGER,
notes TEXT,
link_id INTEGER, -- res_id (linked record)
resModel TEXT, -- 'project.task', 'project.project', etc.
state TEXT, -- 'today', 'planned', 'overdue', 'done'
status TEXT DEFAULT "",
odoo_record_id INTEGER
);CREATE TABLE sync_report (
id INTEGER PRIMARY KEY AUTOINCREMENT,
status TEXT, -- 'In Progress', 'Successful', 'Failed'
account_id INTEGER,
timestamp TEXT,
message TEXT
);CREATE TABLE form_drafts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
draft_type TEXT, -- 'task', 'timesheet', 'project', 'activity'
record_id INTEGER,
account_id INTEGER,
form_data TEXT, -- JSON
original_data TEXT, -- JSON
page_identifier TEXT,
is_new_record INTEGER,
created_at TEXT,
updated_at TEXT
); ┌─────────────────┐
│ Trigger Sync │
│ (Manual/Auto) │
└────────┬────────┘
│
┌────────────────┴────────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ sync_from_odoo.py │ │ sync_to_odoo.py │
│ ─────────────────────│ │ ─────────────────────│
│ 1. Fetch from Odoo │ │ 1. Get local changes │
│ 2. Map fields │ │ 2. Map to Odoo fields│
│ 3. Insert/Replace │ │ 3. Create/Update │
│ 4. Preserve pending │ │ 4. Clear status │
└──────────────────────┘ └──────────────────────┘
| Status | Meaning |
|---|---|
NULL or ''
|
Synced with Odoo |
'created' |
New record, needs upload to Odoo |
'updated' |
Modified locally, needs sync to Odoo |
'deleted' |
Marked for deletion |
-
Local changes take priority during sync from Odoo if
status IN ('created', 'updated') - Favorites and other user preferences are preserved
- Last-modified timestamp comparison for updates
┌─────────────────────────────────────────────────────────────────┐
│ daemon.py (NotificationDaemon) │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ GLib │ │ DBus │ │ Wakelock │ │
│ │ MainLoop │ │ Session │ │ com.lomiri.Repowerd │ │
│ └─────────────┘ └─────────────┘ └──────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Main Loop: │
│ 1. Every 60 seconds: sync_all_accounts() │
│ 2. Check for new tasks/activities │
│ 3. Send push notifications │
│ 4. Update heartbeat file │
└─────────────────────────────────────────────────────────────────┘
-
Startup:
- Write PID to
~/.daemon.pid - Initialize DBus connection
- Request wakelock from Repowerd
- Lower OOM killer priority
- Register for sleep/wake signals
- Write PID to
-
Running:
- GLib main loop with 60-second timer
- For each account: sync and check for updates
- Update heartbeat file (
~/.daemon_heartbeat) - Send notifications for new items
-
Shutdown:
- Release wakelock
- Remove PID file
- Clean exit
// NotificationHelper.cpp
bool NotificationHelper::isDaemonHealthy() {
// Check heartbeat file age (< 5 minutes)
// Check PID file and if process is running
}// Usage in QML
BackendBridge {
id: backend_bridge
module: "backend" // Python module to import
onMessageReceived: function(data) {
// Handle events from Python
if (data.event === "sync_progress") {
progressBar.value = data.payload;
}
}
}
// Calling Python functions
backend_bridge.call("backend.sync_background", [db_path, account_id], function(result) {
console.log("Sync started:", result);
});# Python side
from bus import send
def sync_background(db_path, account_id):
send("sync_progress", 0)
# ... sync logic ...
send("sync_progress", 50)
# ... more sync ...
send("sync_completed", True)# bus.py
try:
import pyotherside
_HAS_PYOTHERSIDE = True
except ImportError:
_HAS_PYOTHERSIDE = False
def send(event_name, payload):
if _HAS_PYOTHERSIDE:
pyotherside.send({'event': event_name, 'payload': payload})-
C++ Plugin (
qml-notify-module/):- Native DBus access
- Postal API for Ubuntu Touch notifications
- Badge count updates
-
Daemon Notifications (
daemon.py):- Triggered on new tasks/activities
- Deep link URIs for navigation
-
In-App Notifications (
notifications.js):- SQLite-backed notification storage
- Notification bell UI component
┌─────────────────┐
│ Daemon detects │
│ new task │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────────────┐
│ Add to local DB │────▶│ notification table │
│ (notifications) │ │ read_status = 0 │
└────────┬────────┘ └─────────────────────┘
│
▼
┌─────────────────┐
│ Send system │
│ notification │
│ via DBus/Postal │
└─────────────────┘
│
▼
┌─────────────────┐
│ User taps │
│ notification │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Deep link opens │
│ app to record │
│ ubtms://navigate│
│ ?type=Task&id=X │
└─────────────────┘
# Install Clickable
pip3 install --user clickable-ut
export PATH=$PATH:~/.local/bin# Run on desktop (uses QML emulation)
clickable desktop
# View logs
tail -f ~/daemon.log# Build and install on connected device
clickable install
# SSH to device and check logs
adb shell
cat /home/phablet/.local/share/ubtms/daemon.log# clickable.yaml
clickable_minimum_required: 8.0.0
builder: cmake
kill: qmlscene
skip_review: true| Log | Location | Purpose |
|---|---|---|
| Daemon log | ~/.local/share/ubtms/daemon.log |
Background sync, notifications |
| App console | QML console.log() output |
UI debugging |
# logger.py
def setup_logger(name="odoo_sync", log_file=None, level=logging.DEBUG):
# Change to DEBUG for verbose output# Check if daemon is running
cat ~/.daemon.pid && ps aux | grep daemon
# Check heartbeat freshness
stat ~/.daemon_heartbeat
# View recent sync reports
sqlite3 ~/.local/share/ubtms/Databases/*.sqlite \
"SELECT * FROM sync_report ORDER BY timestamp DESC LIMIT 5;"
# Check pending sync items
sqlite3 ~/.local/share/ubtms/Databases/*.sqlite \
"SELECT id, name, status FROM project_task_app WHERE status != '';"
# Force restart daemon
kill $(cat ~/.daemon.pid); python3 src/daemon.py// Add to any QML file for debugging
Component.onCompleted: {
console.log("Page loaded, accountId:", accountId);
console.log("Data:", JSON.stringify(modelData, null, 2));
}# Add to sync functions
from logger import setup_logger
log = setup_logger()
log.debug(f"[DEBUG] Processing record: {record}")# Open database with sqlite3
sqlite3 ~/.clickable/home/.local/share/ubtms/Databases/*.sqlite
# List tables
.tables
# Check schema
.schema project_task_app
# Query data
SELECT * FROM project_task_app LIMIT 10;The following files are development/testing utilities that are not needed for production:
| File | Purpose | Safe to Remove |
|---|---|---|
src/autotester.py |
Automated sync testing | ✅ Yes |
src/add_demo_connection.py |
Adds demo Odoo account | ✅ Yes |
src/tool_field_sync.py |
Fetches Odoo field metadata | ✅ Yes |
src/tool_get_remote_record.py |
Debug tool for Odoo records | ✅ Yes |
src/cli.py |
CLI interface (unused) | ✅ Yes |
src/test_data/ |
Test database files | ✅ Yes |
src/odoo_config/ |
Cached Odoo field metadata | ✅ Yes |
src/all_odoo_fields.json |
Duplicate of above | ✅ Yes |
po/nl.po~ |
Backup translation file | ✅ Yes |
cd /home/suraj/timemanagement
# Remove development/testing files
rm -f src/autotester.py
rm -f src/add_demo_connection.py
rm -f src/tool_field_sync.py
rm -f src/tool_get_remote_record.py
rm -f src/cli.py
rm -rf src/test_data/
rm -rf src/odoo_config/
rm -f src/all_odoo_fields.json
rm -f po/nl.po~Symptom: "Authentication failed for server" message
Solution:
- Verify account credentials in Settings
- Check if API key is correct
- Ensure Odoo server is accessible
- Check SSL certificate validity
Symptom: No notifications, no background sync
Solution:
# Check dependencies
sudo apt install python3-dbus python3-gi gir1.2-glib-2.0
# Check for marker file
cat ~/.ubtms_needs_setup
# Manually start daemon
python3 /path/to/src/daemon.pySymptom: "database is locked" in logs
Solution:
- The
safe_sql_execute()function handles retries automatically - If persistent, check for zombie processes:
pkill -f "sqlite.*ubtms"
Symptom: Sync works but no push notifications
Solution:
- Check DBus is available:
echo $DBUS_SESSION_BUS_ADDRESS - Verify Postal service:
dbus-send --print-reply --dest=com.lomiri.Postal /com/lomiri/Postal org.freedesktop.DBus.Introspectable.Introspect - Check daemon logs for notification errors
Symptom: Local changes not appearing in Odoo
Solution:
- Check record has
status = 'created'or'updated' - Verify account is not "Local Account" (id=0)
- Check sync_report table for errors
- Manually trigger sync from Settings
| Version | Changes |
|---|---|
| 1.2.2 | Current version - Multi-assignee support, draft auto-save |
MIT License - Copyright (c) 2025 CIT-Services
Last updated: December 2025