Sync Tempo worklogs to Unit4 Zeiterfassung via Playwright browser automation.
USE AT YOUR OWN RISK. This software is provided "as is" without warranty of any kind.
CDDS AB and the contributors to this project:
- Make no guarantees about the correctness, reliability, or suitability of this software
- Accept no liability for any damages, data loss, or other issues arising from its use
- Provide no support or maintenance obligations
This tool automates browser interactions with Unit4, which may break at any time due to UI changes. Always verify your time entries manually in Unit4 after syncing.
By using this software, you acknowledge that you are solely responsible for any consequences of its use.
| Requirement | Notes |
|---|---|
| OS | Linux / macOS (Windows: use WSL, see below) |
| uv | Python package manager (install) |
| Network | VPN if required for Unit4 access |
Note: Python is managed automatically by
uv— no manual installation needed.
The shell scripts (setup.sh, sync, build-mapping) require a Unix shell.
On Windows, use WSL (Windows Subsystem for Linux):
# 1. Install WSL (run as Administrator)
wsl --install
# 2. Open Ubuntu terminal, then follow Quick Start below# 0. Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 1. Clone and setup
git clone <repo-url>
cd j2u4
./setup.sh
# 2. Edit config.json with your API tokens
# 3. Test connectivity
./sync --check
# 4. Sync a week (dry-run first, then execute)
./sync 202606
./sync 202606 --executeTempo API ──→ Worklogs (date, hours, issue_id)
│
▼
Jira API ──→ Issue Details (key, summary, Account field)
│
▼
Mapping ──→ Tempo Account → Unit4 ArbAuft
│
▼
Playwright ──→ Unit4 Zeiterfassung (browser automation)
./setup.shThis will:
- Install Python and all dependencies (via
uv) - Install Chromium for browser automation
- Create
config.jsonfrom template
-
Install dependencies
uv sync uv run playwright install chromium
-
Create config file
cp config.example.json config.json
You need two API tokens:
- Jira API Token: Create here
- Tempo API Token: Go to Tempo > Settings > API Integration
https://<YOUR-ORG>.atlassian.net/plugins/servlet/ac/io.tempo.jira/tempo-app#!/configuration/api-integration
Edit config.json with your credentials:
{
"jira": {
"base_url": "https://<YOUR-ORG>.atlassian.net",
"user_email": "your-email@example.com",
"api_token": "your-jira-api-token"
},
"tempo": {
"api_token": "your-tempo-api-token"
},
"unit4": {
"url": "https://ubw.unit4cloud.com/<YOUR-TENANT>/Default.aspx"
}
}On first run, Unit4 will prompt for login (2FA). The session is saved to session.json for subsequent runs.
./sync --checkThis tests Jira, Tempo, and Unit4 connectivity before syncing.
# Dry-run (default) - shows what would happen
./sync 202605
# Execute - actually creates entries
./sync 202605 --executeThe week format is YYYYWW (ISO week number), e.g., 202605 = Week 5 of 2026.
- Fetches worklogs from Tempo for the specified week
- Looks up Jira issues to get the Account field
- Maps Account → Unit4 ArbAuft code
- Opens Unit4 in a browser (you can watch!)
- Deletes all existing
[WL:xxx]entries for that week - Creates fresh entries from Tempo
Entries are marked with [WL:xxx] at the beginning of the text field:
[WL:1764] working on concept
This allows tracking which Unit4 entries were synced from which Tempo worklog.
| File | Purpose |
|---|---|
setup.sh |
One-time setup (creates venv, installs dependencies) |
sync |
Wrapper script for syncing (use this!) |
build-mapping |
Wrapper script for building mappings |
sync_tempo_to_unit4.py |
Main sync script (Python) |
build_mapping_from_history.py |
Build account→arbauft mapping from Unit4 history |
config.json |
Credentials (gitignored!) |
config.example.json |
Template for config.json |
account_to_arbauft_mapping.json |
Account to ArbAuft mapping (gitignored!) |
session.json |
Browser session (gitignored!) |
The script needs to know which Tempo Account maps to which Unit4 ArbAuft code.
This mapping is stored in account_to_arbauft_mapping.json.
If you already have time entries in Unit4, the script can learn the mappings:
# Scan last 8 weeks (default)
./build-mapping
# Scan last 12 weeks
./build-mapping --weeks 12
# Scan specific range
./build-mapping --from 202601 --to 202610This opens Unit4, scans the specified weeks, and builds the mapping automatically.
When the script encounters an unknown Tempo account, it will prompt you:
Unknown Account: 42 (ACME - Development)
Ticket: ACME-1234
Summary: Fix deployment pipeline
Enter ArbAuft (e.g., 1234-56789-001) or SKIP to skip:
Enter the ArbAuft code and it will be saved for future use.
Edit account_to_arbauft_mapping.json directly:
{
"42": {
"unit4_arbauft": "1234-56789-001",
"tempo_name": "ACME - Development",
"sample_ticket": "ACME-1234"
}
}The ArbAuft code (e.g., 1234-56789-001) is visible in Unit4 when you create a time entry.
It's the "ArbAuft" field in the entry form.
| Command | Description |
|---|---|
./setup.sh |
Initial setup (run once after cloning) |
./sync --check |
Test connectivity to Jira, Tempo, Unit4 |
./sync YYYYWW |
Dry-run sync for week (e.g., ./sync 202606) |
./sync YYYYWW --execute |
Actually sync the week |
./sync YYYYWW --cutover YYYY-MM-DD --execute |
Sync from cutover date onwards |
./build-mapping |
Build mappings from last 8 weeks |
./build-mapping --weeks N |
Build mappings from last N weeks |
./build-mapping --from YYYYWW --to YYYYWW |
Build mappings from specific range |
- Run
./setup.shto create from template, or - Copy manually:
cp config.example.json config.json
- Run
./sync --checkto diagnose connectivity issues - Verify your API tokens are correct in
config.json - Jira token: Check it's not expired at Atlassian Account
- Tempo token: Regenerate in Tempo Settings > API Integration
- Make sure you're connected to VPN (if required)
- Check the URL in
config.jsonis correct
- The script waits for the page to load, but Unit4 can be slow
- If it times out, try running again
- The script deletes all
[WL:xxx]entries before creating new ones - If duplicates appear, run the script again to clean them up
- The script will detect this and prompt for re-login
- If issues persist, delete
session.jsonand run again
- The browser automation works with both German and English Unit4 UI
- Most selectors use stable element IDs; remaining text-based selectors try both languages automatically
- If you encounter issues with a different UI language, run the UI inspector and share the output:
This opens Unit4, scans all UI elements, and saves their HTML attributes to
uv run python inspect_ui.py
ui_inspection.json.
Since Unit4 is a live enterprise system with no sandbox or staging environment, full end-to-end tests are not feasible. The test suite therefore focuses on what can be verified offline:
# Install dev dependencies (once)
uv sync --extra dev
# Run tests
uv run pytestWhat is tested:
- Regex patterns (day labels, worklog markers, ticket keys, ArbAuft codes) against both German and English inputs
- Locale configuration consistency (both locales define the same keys, non-empty values)
What requires manual verification:
- Browser automation against a live Unit4 instance (
./sync --check, then./sync YYYYWW) - Session handling, login flow, 2FA
- Never commit
config.jsonorsession.json - These files are in
.gitignore - Use
config.example.jsonas template
This project is licensed under the MIT License - see the LICENSE file for details.