Initial Kimai 2.61.0 setup with Kimai 1 migration
Some checks failed
Frontend / Frontend verification (push) Has been cancelled
Lint PHP / Linting (8.2) (push) Has been cancelled
Check .lock files / Verify lock file integrity (push) Has been cancelled
Release Drafter / Verify repository (push) Has been cancelled
Release Drafter / Draft next release (push) Has been cancelled
Tests / Integration (8.2) (push) Has been cancelled
Tests / Integration (8.3) (push) Has been cancelled
Tests / Integration (8.4) (push) Has been cancelled
Tests / Integration (8.5) (push) Has been cancelled
Actions Security Analysis / Scan workflows (push) Has been cancelled
Some checks failed
Frontend / Frontend verification (push) Has been cancelled
Lint PHP / Linting (8.2) (push) Has been cancelled
Check .lock files / Verify lock file integrity (push) Has been cancelled
Release Drafter / Verify repository (push) Has been cancelled
Release Drafter / Draft next release (push) Has been cancelled
Tests / Integration (8.2) (push) Has been cancelled
Tests / Integration (8.3) (push) Has been cancelled
Tests / Integration (8.4) (push) Has been cancelled
Tests / Integration (8.5) (push) Has been cancelled
Actions Security Analysis / Scan workflows (push) Has been cancelled
- PHP 8.3, Symfony 6.4, MariaDB 10.11 - Migrated from Kimai 1.x: 908K timesheets, 2,154 users, 427 projects - Custom migration script at migrate.php - All users login with: changeme
This commit is contained in:
656
migrate.php
Normal file
656
migrate.php
Normal file
@@ -0,0 +1,656 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Kimai v1 → Kimai v2 Migration Script
|
||||
*
|
||||
* Reads from `kimai` database (prefix: kimai15_)
|
||||
* Writes to `kimai2` database (prefix: kimai2_)
|
||||
*
|
||||
* Usage: php8.3 /root/workspace/kimai2/migrate.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// ── Config ──────────────────────────────────────────────────────────────────
|
||||
const SOURCE_DSN = 'mysql:host=localhost;dbname=kimai;charset=utf8mb4';
|
||||
const TARGET_DSN = 'mysql:host=localhost;dbname=kimai2;charset=utf8mb4';
|
||||
const DB_USER = 'root';
|
||||
const DB_PASS = '';
|
||||
|
||||
const DEFAULT_PASSWORD = 'changeme';
|
||||
const DEFAULT_TIMEZONE = 'UTC';
|
||||
const DEFAULT_CURRENCY = 'EUR';
|
||||
const DEFAULT_COUNTRY = '';
|
||||
const BATCH_SIZE = 2000; // rows per INSERT batch
|
||||
|
||||
// ── Bootstrap ───────────────────────────────────────────────────────────────
|
||||
$src = new PDO(SOURCE_DSN, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
|
||||
$dst = new PDO(TARGET_DSN, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
|
||||
// Disable foreign key checks and autocommit for speed
|
||||
$dst->exec('SET FOREIGN_KEY_CHECKS = 0');
|
||||
$dst->exec('SET AUTOCOMMIT = 0');
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
function progress(string $label, int $done, int $total): void {
|
||||
$pct = $total > 0 ? round($done / $total * 100, 1) : 100.0;
|
||||
printf("\r %s: %d / %d (%s%%)", str_pad($label, 20), $done, $total, $pct);
|
||||
}
|
||||
|
||||
function done(string $label, int $count, float $elapsed): void {
|
||||
printf("\r %s: %d rows (%.2f s)\n", str_pad($label, 20), $count, $elapsed);
|
||||
}
|
||||
|
||||
function batchInsert(PDO $dst, string $table, array $columns, array $rows, int $batchSize = BATCH_SIZE): int {
|
||||
if (empty($rows)) {
|
||||
return 0;
|
||||
}
|
||||
$total = 0;
|
||||
$placeholders = '(' . implode(',', array_fill(0, count($columns), '?')) . ')';
|
||||
$sql = sprintf(
|
||||
'INSERT INTO `%s` (`%s`) VALUES %s',
|
||||
$table,
|
||||
implode('`, `', $columns),
|
||||
implode(',', array_fill(0, min($batchSize, count($rows)), $placeholders))
|
||||
);
|
||||
|
||||
foreach (array_chunk($rows, $batchSize) as $chunk) {
|
||||
// Rebuild SQL if chunk is smaller than batchSize (last chunk)
|
||||
if (count($chunk) < $batchSize) {
|
||||
$sql = sprintf(
|
||||
'INSERT INTO `%s` (`%s`) VALUES %s',
|
||||
$table,
|
||||
implode('`, `', $columns),
|
||||
implode(',', array_fill(0, count($chunk), $placeholders))
|
||||
);
|
||||
}
|
||||
$flat = [];
|
||||
foreach ($chunk as $row) {
|
||||
foreach ($row as $val) {
|
||||
$flat[] = $val;
|
||||
}
|
||||
}
|
||||
$stmt = $dst->prepare($sql);
|
||||
$stmt->execute($flat);
|
||||
$total += count($chunk);
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
function safeString(?string $val, int $maxLen = 255): ?string {
|
||||
if ($val === null || $val === '') {
|
||||
return null;
|
||||
}
|
||||
$val = trim($val);
|
||||
if ($val === '') {
|
||||
return null;
|
||||
}
|
||||
return mb_substr($val, 0, $maxLen);
|
||||
}
|
||||
|
||||
function timestampToDateTime(int $ts): ?string {
|
||||
if ($ts <= 0) {
|
||||
return null;
|
||||
}
|
||||
return date('Y-m-d H:i:s', $ts);
|
||||
}
|
||||
|
||||
// ── Truncate target tables first ────────────────────────────────────────────
|
||||
echo "Clearing target tables...\n";
|
||||
$tables = [
|
||||
'kimai2_timesheet_tags', 'kimai2_timesheet_meta', 'kimai2_timesheet',
|
||||
'kimai2_activities_rates', 'kimai2_activities_teams', 'kimai2_activities_meta', 'kimai2_activities',
|
||||
'kimai2_projects_rates', 'kimai2_projects_teams', 'kimai2_projects_comments', 'kimai2_projects_meta', 'kimai2_projects',
|
||||
'kimai2_customers_rates', 'kimai2_customers_teams', 'kimai2_customers_comments', 'kimai2_customers_meta', 'kimai2_customers',
|
||||
'kimai2_users_teams', 'kimai2_user_preferences', 'kimai2_users',
|
||||
'kimai2_teams',
|
||||
];
|
||||
foreach ($tables as $table) {
|
||||
try {
|
||||
$dst->exec("DELETE FROM `$table`");
|
||||
echo " Cleared: $table\n";
|
||||
} catch (PDOException $e) {
|
||||
echo " Skipped (doesn't exist): $table\n";
|
||||
}
|
||||
}
|
||||
$dst->exec('COMMIT');
|
||||
echo "\n";
|
||||
|
||||
// ── Migration order: master data first, then transactions ───────────────────
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ 1. Groups → Teams ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
echo "--- Migrating Groups → Teams ---\n";
|
||||
|
||||
$t0 = microtime(true);
|
||||
$groups = $src->query('SELECT grp_ID, grp_name FROM kimai15_grp WHERE grp_trash = 0')->fetchAll();
|
||||
$rows = [];
|
||||
foreach ($groups as $g) {
|
||||
$rows[] = [
|
||||
'id' => (int) $g['grp_ID'],
|
||||
'name' => safeString($g['grp_name'], 100) ?? '',
|
||||
'color' => null,
|
||||
];
|
||||
}
|
||||
$count = batchInsert($dst, 'kimai2_teams', ['id', 'name', 'color'], $rows);
|
||||
$dst->exec('COMMIT');
|
||||
done('Teams', $count, microtime(true) - $t0);
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ 2. Customers ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
echo "--- Migrating Customers ---\n";
|
||||
|
||||
$t0 = microtime(true);
|
||||
$customers = $src->query('SELECT * FROM kimai15_knd WHERE knd_trash = 0')->fetchAll();
|
||||
$total = count($customers);
|
||||
$count = 0;
|
||||
|
||||
$rows = [];
|
||||
foreach ($customers as $c) {
|
||||
$rows[] = [
|
||||
'id' => (int) $c['knd_ID'],
|
||||
'name' => safeString($c['knd_name'], 150) ?: '',
|
||||
'number' => null,
|
||||
'comment' => $c['knd_comment'] ?: null,
|
||||
'visible' => (int) $c['knd_visible'],
|
||||
'company' => safeString($c['knd_company'], 100),
|
||||
'contact' => safeString($c['knd_contact'], 100),
|
||||
'address' => $c['knd_street'] ?: null,
|
||||
'country' => DEFAULT_COUNTRY,
|
||||
'currency' => DEFAULT_CURRENCY,
|
||||
'phone' => safeString($c['knd_tel'], 30),
|
||||
'fax' => safeString($c['knd_fax'], 30),
|
||||
'mobile' => safeString($c['knd_mobile'], 30),
|
||||
'email' => safeString($c['knd_mail'], 75),
|
||||
'homepage' => safeString($c['knd_homepage'], 100),
|
||||
'timezone' => DEFAULT_TIMEZONE,
|
||||
'color' => null,
|
||||
'time_budget' => 0,
|
||||
'budget' => 0.0,
|
||||
'vat_id' => safeString($c['knd_vat'], 50),
|
||||
'budget_type' => null,
|
||||
'billable' => 1,
|
||||
'invoice_template_id' => null,
|
||||
'invoice_text' => null,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'address_line1' => safeString($c['knd_street'], 150),
|
||||
'address_line2' => null,
|
||||
'address_line3' => null,
|
||||
'postcode' => safeString($c['knd_zipcode'], 20),
|
||||
'city' => safeString($c['knd_city'], 50),
|
||||
'buyer_reference' => null,
|
||||
];
|
||||
$count++;
|
||||
if ($count % 100 === 0) {
|
||||
progress('Customers', $count, $total);
|
||||
}
|
||||
}
|
||||
$imported = batchInsert($dst, 'kimai2_customers', [
|
||||
'id', 'name', 'number', 'comment', 'visible', 'company', 'contact', 'address',
|
||||
'country', 'currency', 'phone', 'fax', 'mobile', 'email', 'homepage',
|
||||
'timezone', 'color', 'time_budget', 'budget', 'vat_id', 'budget_type',
|
||||
'billable', 'invoice_template_id', 'invoice_text', 'created_at',
|
||||
'address_line1', 'address_line2', 'address_line3', 'postcode', 'city', 'buyer_reference',
|
||||
], $rows);
|
||||
$dst->exec('COMMIT');
|
||||
done('Customers', $imported, microtime(true) - $t0);
|
||||
|
||||
// Reset auto-increment after inserting with explicit IDs
|
||||
$dst->exec("ALTER TABLE kimai2_customers AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(knd_ID) FROM kimai15_knd')->fetchColumn() + 1));
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ 3. Projects ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
echo "--- Migrating Projects ---\n";
|
||||
|
||||
$t0 = microtime(true);
|
||||
$projects = $src->query('SELECT * FROM kimai15_pct WHERE pct_trash = 0')->fetchAll();
|
||||
$total = count($projects);
|
||||
$count = 0;
|
||||
|
||||
$rows = [];
|
||||
foreach ($projects as $p) {
|
||||
$rows[] = [
|
||||
'id' => (int) $p['pct_ID'],
|
||||
'customer_id' => (int) $p['pct_kndID'],
|
||||
'name' => safeString($p['pct_name'], 150) ?: '',
|
||||
'order_number'=> null,
|
||||
'comment' => $p['pct_comment'] ?: null,
|
||||
'visible' => (int) $p['pct_visible'],
|
||||
'budget' => (float) $p['pct_budget'],
|
||||
'color' => null,
|
||||
'time_budget' => 0,
|
||||
'order_date' => null,
|
||||
'start' => null,
|
||||
'end' => null,
|
||||
'timezone' => null,
|
||||
'budget_type' => null,
|
||||
'billable' => (int) $p['pct_billable'],
|
||||
'invoice_text'=> null,
|
||||
'global_activities' => 1,
|
||||
'number' => null,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
$count++;
|
||||
if ($count % 100 === 0) {
|
||||
progress('Projects', $count, $total);
|
||||
}
|
||||
}
|
||||
$imported = batchInsert($dst, 'kimai2_projects', [
|
||||
'id', 'customer_id', 'name', 'order_number', 'comment', 'visible',
|
||||
'budget', 'color', 'time_budget', 'order_date', 'start', 'end',
|
||||
'timezone', 'budget_type', 'billable', 'invoice_text',
|
||||
'global_activities', 'number', 'created_at',
|
||||
], $rows);
|
||||
$dst->exec('COMMIT');
|
||||
done('Projects', $imported, microtime(true) - $t0);
|
||||
|
||||
$dst->exec("ALTER TABLE kimai2_projects AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(pct_ID) FROM kimai15_pct')->fetchColumn() + 1));
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ 4. Activities (global) ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
echo "--- Migrating Activities ---\n";
|
||||
|
||||
$t0 = microtime(true);
|
||||
$activities = $src->query('SELECT * FROM kimai15_evt WHERE evt_trash = 0')->fetchAll();
|
||||
$total = count($activities);
|
||||
$count = 0;
|
||||
|
||||
$rows = [];
|
||||
foreach ($activities as $a) {
|
||||
$rows[] = [
|
||||
'id' => (int) $a['evt_ID'],
|
||||
'project_id' => null, // global activities
|
||||
'name' => safeString($a['evt_name'], 150) ?: '',
|
||||
'comment' => $a['evt_comment'] ?: null,
|
||||
'visible' => (int) $a['evt_visible'],
|
||||
'color' => null,
|
||||
'time_budget'=> 0,
|
||||
'budget' => 0.0,
|
||||
'budget_type'=> null,
|
||||
'billable' => 1,
|
||||
'invoice_text'=> null,
|
||||
'number' => null,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
$count++;
|
||||
if ($count % 100 === 0) {
|
||||
progress('Activities', $count, $total);
|
||||
}
|
||||
}
|
||||
$imported = batchInsert($dst, 'kimai2_activities', [
|
||||
'id', 'project_id', 'name', 'comment', 'visible', 'color',
|
||||
'time_budget', 'budget', 'budget_type', 'billable', 'invoice_text',
|
||||
'number', 'created_at',
|
||||
], $rows);
|
||||
$dst->exec('COMMIT');
|
||||
done('Activities', $imported, microtime(true) - $t0);
|
||||
|
||||
$dst->exec("ALTER TABLE kimai2_activities AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(evt_ID) FROM kimai15_evt')->fetchColumn() + 1));
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ 5. Users ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
echo "--- Migrating Users ---\n";
|
||||
|
||||
$t0 = microtime(true);
|
||||
$users = $src->query('SELECT * FROM kimai15_usr ORDER BY usr_ID')->fetchAll();
|
||||
$total = count($users);
|
||||
$count = 0;
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$passwordHash = password_hash(DEFAULT_PASSWORD, PASSWORD_BCRYPT);
|
||||
|
||||
$userRows = [];
|
||||
$prefRows = [];
|
||||
$teamRows = [];
|
||||
|
||||
$seenEmails = [];
|
||||
$seenUsernames = [];
|
||||
|
||||
foreach ($users as $u) {
|
||||
$userId = (int) $u['usr_ID'];
|
||||
$username = safeString($u['usr_name'], 180) ?: 'user_' . $userId;
|
||||
$alias = safeString($u['usr_alias'], 60);
|
||||
$email = safeString($u['usr_mail'], 180) ?: ($username . '@migrated.local');
|
||||
$enabled = (int) $u['usr_active'];
|
||||
$v1group = (int) $u['usr_grp'];
|
||||
|
||||
// Deduplicate username (case-insensitive)
|
||||
$origUsername = $username;
|
||||
$suffix = 1;
|
||||
while (isset($seenUsernames[mb_strtolower($username)])) {
|
||||
$username = mb_substr($origUsername, 0, 170) . '_' . $suffix;
|
||||
$suffix++;
|
||||
}
|
||||
$seenUsernames[mb_strtolower($username)] = true;
|
||||
|
||||
// Deduplicate email
|
||||
$origEmail = $email;
|
||||
$suffix = 1;
|
||||
$emailLower = mb_strtolower($email);
|
||||
while (isset($seenEmails[$emailLower])) {
|
||||
$atPos = strpos($origEmail, '@');
|
||||
if ($atPos !== false) {
|
||||
$email = mb_substr($origEmail, 0, min($atPos, 170)) . '+' . $suffix . mb_substr($origEmail, $atPos);
|
||||
} else {
|
||||
$email = mb_substr($origEmail, 0, 170) . '+' . $suffix . '@migrated.local';
|
||||
}
|
||||
$email = mb_substr($email, 0, 180);
|
||||
$emailLower = mb_strtolower($email);
|
||||
$suffix++;
|
||||
}
|
||||
$seenEmails[$emailLower] = true;
|
||||
|
||||
// Role mapping: admin user gets SUPER_ADMIN, others get USER
|
||||
if ($origUsername === 'admin' || $origUsername === 'administrator') {
|
||||
$roles = serialize(['ROLE_SUPER_ADMIN']);
|
||||
} else {
|
||||
$roles = serialize(['ROLE_USER']);
|
||||
}
|
||||
|
||||
$userRows[] = [
|
||||
'id' => $userId,
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'password' => $passwordHash,
|
||||
'alias' => $alias,
|
||||
'enabled' => $enabled,
|
||||
'registration_date' => $now,
|
||||
'title' => null,
|
||||
'avatar' => null,
|
||||
'roles' => $roles,
|
||||
'last_login' => null,
|
||||
'confirmation_token' => null,
|
||||
'password_requested_at' => null,
|
||||
'api_token' => null,
|
||||
'auth' => 'kimai',
|
||||
'color' => null,
|
||||
'account' => null,
|
||||
'totp_secret' => null,
|
||||
'totp_enabled' => 0,
|
||||
'system_account' => 0,
|
||||
'supervisor_id' => null,
|
||||
'signature_date' => null,
|
||||
];
|
||||
|
||||
// User preferences: hourly_rate
|
||||
$prefRows[] = [
|
||||
'user_id' => $userId,
|
||||
'name' => 'hourly_rate',
|
||||
'value' => '0',
|
||||
];
|
||||
$prefRows[] = [
|
||||
'user_id' => $userId,
|
||||
'name' => 'timezone',
|
||||
'value' => DEFAULT_TIMEZONE,
|
||||
];
|
||||
|
||||
// Team membership
|
||||
if ($v1group > 0) {
|
||||
$teamRows[] = [
|
||||
'user_id' => $userId,
|
||||
'team_id' => $v1group,
|
||||
'teamlead' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$count++;
|
||||
if ($count % 100 === 0) {
|
||||
progress('Users', $count, $total);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify no duplicate emails before insert
|
||||
$emailCheck = [];
|
||||
$dupFound = false;
|
||||
foreach ($userRows as $r) {
|
||||
$em = mb_strtolower($r['email']);
|
||||
if (isset($emailCheck[$em])) {
|
||||
echo "\n *** DUPLICATE EMAIL: $em (IDs: {$emailCheck[$em]} and {$r['id']})\n";
|
||||
$dupFound = true;
|
||||
}
|
||||
$emailCheck[$em] = $r['id'];
|
||||
}
|
||||
echo " Verified: " . count($userRows) . " users, " . count($emailCheck) . " unique emails\n";
|
||||
if ($dupFound) {
|
||||
echo " ERROR: Duplicate emails detected, aborting.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$imported = batchInsert($dst, 'kimai2_users', [
|
||||
'id', 'username', 'email', 'password', 'alias', 'enabled',
|
||||
'registration_date', 'title', 'avatar', 'roles', 'last_login',
|
||||
'confirmation_token', 'password_requested_at', 'api_token', 'auth',
|
||||
'color', 'account', 'totp_secret', 'totp_enabled', 'system_account',
|
||||
'supervisor_id', 'signature_date',
|
||||
], $userRows);
|
||||
$dst->exec('COMMIT');
|
||||
done('Users', $imported, microtime(true) - $t0);
|
||||
$dst->exec("ALTER TABLE kimai2_users AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(usr_ID) FROM kimai15_usr')->fetchColumn() + 1));
|
||||
|
||||
// User preferences
|
||||
echo "--- Migrating User Preferences ---\n";
|
||||
$t0 = microtime(true);
|
||||
$imported = batchInsert($dst, 'kimai2_user_preferences', ['user_id', 'name', 'value'], $prefRows);
|
||||
$dst->exec('COMMIT');
|
||||
done('User Preferences', $imported, microtime(true) - $t0);
|
||||
|
||||
// User-Team mappings
|
||||
echo "--- Migrating User-Team Mappings ---\n";
|
||||
$t0 = microtime(true);
|
||||
$imported = batchInsert($dst, 'kimai2_users_teams', ['user_id', 'team_id', 'teamlead'], $teamRows);
|
||||
$dst->exec('COMMIT');
|
||||
done('User-Team Mappings', $imported, microtime(true) - $t0);
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ 6. Rates ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
echo "--- Migrating Rates ---\n";
|
||||
|
||||
$t0 = microtime(true);
|
||||
$rates = $src->query('SELECT * FROM kimai15_rates')->fetchAll();
|
||||
$projectRates = [];
|
||||
$activityRates = [];
|
||||
|
||||
foreach ($rates as $r) {
|
||||
if (!empty($r['project_id'])) {
|
||||
$projectRates[] = [
|
||||
'user_id' => $r['user_id'] ? (int) $r['user_id'] : null,
|
||||
'project_id' => (int) $r['project_id'],
|
||||
'rate' => (float) $r['rate'],
|
||||
'fixed' => 0,
|
||||
'internal_rate' => null,
|
||||
];
|
||||
}
|
||||
if (!empty($r['event_id'])) {
|
||||
$activityRates[] = [
|
||||
'user_id' => $r['user_id'] ? (int) $r['user_id'] : null,
|
||||
'activity_id' => (int) $r['event_id'],
|
||||
'rate' => (float) $r['rate'],
|
||||
'fixed' => 0,
|
||||
'internal_rate' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$count1 = batchInsert($dst, 'kimai2_projects_rates', ['user_id', 'project_id', 'rate', 'fixed', 'internal_rate'], $projectRates);
|
||||
$count2 = batchInsert($dst, 'kimai2_activities_rates', ['user_id', 'activity_id', 'rate', 'fixed', 'internal_rate'], $activityRates);
|
||||
$dst->exec('COMMIT');
|
||||
done('Rates (projects)', $count1, microtime(true) - $t0);
|
||||
done('Rates (activities)', $count2, microtime(true) - $t0);
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ 7. Timesheets (the big one: ~908K rows) ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
echo "--- Migrating Timesheets (~908K rows) ---\n";
|
||||
|
||||
$t0 = microtime(true);
|
||||
|
||||
// Count total
|
||||
$total = (int) $src->query('SELECT COUNT(*) FROM kimai15_zef')->fetchColumn();
|
||||
$count = 0;
|
||||
|
||||
// Stream with unbuffered query
|
||||
$stmt = $src->prepare('SELECT zef_ID, zef_in, zef_out, zef_time, zef_usrID, zef_pctID, zef_evtID, zef_comment, zef_rate FROM kimai15_zef ORDER BY zef_ID');
|
||||
$stmt->execute();
|
||||
|
||||
$rows = [];
|
||||
while ($row = $stmt->fetch()) {
|
||||
$startTime = timestampToDateTime((int) $row['zef_in']);
|
||||
|
||||
// Skip entries with no valid start time
|
||||
if ($startTime === null) {
|
||||
$count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$endTime = timestampToDateTime((int) $row['zef_out']);
|
||||
$duration = (int) $row['zef_time']; // seconds
|
||||
|
||||
// date_tz: extract date portion from start_time
|
||||
$dateTz = $startTime ? substr($startTime, 0, 10) : null;
|
||||
|
||||
$rows[] = [
|
||||
'id' => (int) $row['zef_ID'],
|
||||
'user' => (int) $row['zef_usrID'],
|
||||
'activity_id' => (int) $row['zef_evtID'],
|
||||
'project_id' => (int) $row['zef_pctID'],
|
||||
'start_time' => $startTime,
|
||||
'end_time' => $endTime,
|
||||
'duration' => $duration > 0 ? $duration : null,
|
||||
'description' => $row['zef_comment'] ?: null,
|
||||
'rate' => (float) $row['zef_rate'],
|
||||
'fixed_rate' => null,
|
||||
'hourly_rate' => null,
|
||||
'exported' => 0,
|
||||
'timezone' => DEFAULT_TIMEZONE,
|
||||
'internal_rate'=> null,
|
||||
'billable' => 1,
|
||||
'category' => 'work',
|
||||
'modified_at' => null,
|
||||
'date_tz' => $dateTz,
|
||||
'break' => null,
|
||||
];
|
||||
|
||||
$count++;
|
||||
if ($count % BATCH_SIZE === 0) {
|
||||
batchInsert($dst, 'kimai2_timesheet', [
|
||||
'id', 'user', 'activity_id', 'project_id', 'start_time', 'end_time',
|
||||
'duration', 'description', 'rate', 'fixed_rate', 'hourly_rate',
|
||||
'exported', 'timezone', 'internal_rate', 'billable', 'category',
|
||||
'modified_at', 'date_tz', 'break',
|
||||
], $rows, BATCH_SIZE);
|
||||
$dst->exec('COMMIT');
|
||||
$rows = [];
|
||||
progress('Timesheets', $count, $total);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert final batch
|
||||
if (!empty($rows)) {
|
||||
batchInsert($dst, 'kimai2_timesheet', [
|
||||
'id', 'user', 'activity_id', 'project_id', 'start_time', 'end_time',
|
||||
'duration', 'description', 'rate', 'fixed_rate', 'hourly_rate',
|
||||
'exported', 'timezone', 'internal_rate', 'billable', 'category',
|
||||
'modified_at', 'date_tz', 'break',
|
||||
], $rows, BATCH_SIZE);
|
||||
$dst->exec('COMMIT');
|
||||
}
|
||||
|
||||
done('Timesheets', $count, microtime(true) - $t0);
|
||||
$dst->exec("ALTER TABLE kimai2_timesheet AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(zef_ID) FROM kimai15_zef')->fetchColumn() + 1));
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ 8. Expenses (optional) ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
echo "--- Migrating Expenses ---\n";
|
||||
|
||||
$t0 = microtime(true);
|
||||
$expenses = $src->query('SELECT * FROM kimai15_exp')->fetchAll();
|
||||
$total = count($expenses);
|
||||
|
||||
// Expenses are stored as timesheet entries with category='expense' in Kimai2
|
||||
// Expenses: need a fallback activity_id since Kimai2 requires it NOT NULL
|
||||
$fallbackActivityId = (int) ($dst->query('SELECT MIN(id) FROM kimai2_activities')->fetchColumn() ?: 1);
|
||||
|
||||
$rows = [];
|
||||
foreach ($expenses as $e) {
|
||||
$startTime = timestampToDateTime((int) $e['exp_timestamp']);
|
||||
$dateTz = $startTime ? substr($startTime, 0, 10) : null;
|
||||
|
||||
$rows[] = [
|
||||
'user' => (int) $e['exp_usrID'],
|
||||
'activity_id' => $fallbackActivityId,
|
||||
'project_id' => (int) $e['exp_pctID'],
|
||||
'start_time' => $startTime,
|
||||
'end_time' => $startTime, // expenses: same as start
|
||||
'duration' => null,
|
||||
'description' => ($e['exp_designation'] ?: '') . ($e['exp_comment'] ? ' — ' . $e['exp_comment'] : ''),
|
||||
'rate' => (float) $e['exp_value'],
|
||||
'fixed_rate' => null,
|
||||
'hourly_rate' => null,
|
||||
'exported' => (int) $e['exp_cleared'],
|
||||
'timezone' => DEFAULT_TIMEZONE,
|
||||
'internal_rate'=> null,
|
||||
'billable' => (int) $e['exp_refundable'],
|
||||
'category' => 'expense',
|
||||
'modified_at' => null,
|
||||
'date_tz' => $dateTz,
|
||||
'break' => null,
|
||||
];
|
||||
}
|
||||
$imported = batchInsert($dst, 'kimai2_timesheet', [
|
||||
'user', 'activity_id', 'project_id', 'start_time', 'end_time',
|
||||
'duration', 'description', 'rate', 'fixed_rate', 'hourly_rate',
|
||||
'exported', 'timezone', 'internal_rate', 'billable', 'category',
|
||||
'modified_at', 'date_tz', 'break',
|
||||
], $rows);
|
||||
$dst->exec('COMMIT');
|
||||
done('Expenses', $imported, microtime(true) - $t0);
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ Re-enable foreign keys ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
$dst->exec('SET FOREIGN_KEY_CHECKS = 1');
|
||||
$dst->exec('SET AUTOCOMMIT = 1');
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ Summary ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
echo "\n";
|
||||
echo str_repeat('=', 60) . "\n";
|
||||
echo "MIGRATION SUMMARY\n";
|
||||
echo str_repeat('=', 60) . "\n";
|
||||
|
||||
$summaryTables = [
|
||||
'Teams' => 'kimai2_teams',
|
||||
'Customers' => 'kimai2_customers',
|
||||
'Projects' => 'kimai2_projects',
|
||||
'Activities' => 'kimai2_activities',
|
||||
'Users' => 'kimai2_users',
|
||||
'User Preferences' => 'kimai2_user_preferences',
|
||||
'User-Team Mappings'=> 'kimai2_users_teams',
|
||||
'Timesheets' => 'kimai2_timesheet',
|
||||
];
|
||||
|
||||
foreach ($summaryTables as $label => $table) {
|
||||
$cnt = $dst->query("SELECT COUNT(*) FROM `$table`")->fetchColumn();
|
||||
printf(" %-20s → %s: %d rows\n", $label, $table, $cnt);
|
||||
}
|
||||
|
||||
echo "\nDefault password for all users: " . DEFAULT_PASSWORD . "\n";
|
||||
echo "Password hash algorithm: BCRYPT\n";
|
||||
echo "\nDone.\n";
|
||||
Reference in New Issue
Block a user