allow missing client columns in clockify importer, fixes #1149

This commit is contained in:
Gregor Vostrak
2026-07-09 16:16:25 +02:00
parent ba374c0371
commit 4fb18f343f
7 changed files with 74 additions and 5 deletions

View File

@@ -29,7 +29,8 @@ class ClockifyProjectsImporter extends DefaultImporter
$records = $reader->getRecords(); $records = $reader->getRecords();
foreach ($records as $record) { foreach ($records as $record) {
$clientId = null; $clientId = null;
if ($record['Client'] !== '') { // Newer Clockify exports no longer contain a "Client" column.
if (($record['Client'] ?? '') !== '') {
$clientId = $this->clientImportHelper->getKey([ $clientId = $this->clientImportHelper->getKey([
'name' => $record['Client'], 'name' => $record['Client'],
'organization_id' => $this->organization->id, 'organization_id' => $this->organization->id,
@@ -45,7 +46,7 @@ class ClockifyProjectsImporter extends DefaultImporter
'color' => $this->colorService->getRandomColor(), 'color' => $this->colorService->getRandomColor(),
'is_billable' => $record['Billability'] === 'Yes', 'is_billable' => $record['Billability'] === 'Yes',
'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null, 'billable_rate' => $billableRateKey !== null && $record[$billableRateKey] !== '' ? (int) (((float) $record[$billableRateKey]) * 100) : null,
'estimated_time' => $record['Estimated (h)'] !== '' && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null, 'estimated_time' => isset($record['Estimated (h)']) && is_numeric($record['Estimated (h)']) ? (int) ($record['Estimated (h)'] * 3600) : null,
'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null, 'archived_at' => $record['Status'] === 'Archived' ? Carbon::now() : null,
]); ]);
} }
@@ -80,7 +81,6 @@ class ClockifyProjectsImporter extends DefaultImporter
{ {
$requiredFields = [ $requiredFields = [
'Project', 'Project',
'Client',
'Status', 'Status',
'Visibility', 'Visibility',
'Billability', 'Billability',

View File

@@ -72,7 +72,7 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
]); ]);
$member = $this->memberImportHelper->getModelById($memberId); $member = $this->memberImportHelper->getModelById($memberId);
$clientId = null; $clientId = null;
if ($record['Client'] !== '') { if (($record['Client'] ?? '') !== '') {
$clientId = $this->clientImportHelper->getKey([ $clientId = $this->clientImportHelper->getKey([
'name' => $record['Client'], 'name' => $record['Client'],
'organization_id' => $this->organization->id, 'organization_id' => $this->organization->id,
@@ -215,7 +215,6 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
{ {
$requiredFields = [ $requiredFields = [
'Project', 'Project',
'Client',
'Description', 'Description',
'User', 'User',
'Group', 'Group',

View File

@@ -0,0 +1,2 @@
"Project","Status","Visibility","Billability","Tasks","Tracked (h)","Estimated (h)","Remaining (h)","Overage (h)","Progress(%)","Billable (h)","Non-billable (h)","Billable Rate (USD)","Amount (USD)","Project members","Project manager","Note"
"Project Without Client Column","Active","Public","Yes","Task 1, Task 2","0.00","100.00","","","","0.00","0.00","100.01","0.00","Constantin Graf","",""
1 Project Status Visibility Billability Tasks Tracked (h) Estimated (h) Remaining (h) Overage (h) Progress(%) Billable (h) Non-billable (h) Billable Rate (USD) Amount (USD) Project members Project manager Note
2 Project Without Client Column Active Public Yes Task 1, Task 2 0.00 100.00 0.00 0.00 100.01 0.00 Constantin Graf

View File

@@ -0,0 +1,3 @@
"Project","Description","Task","User","Group","Email","Tags","Billable","Start Date","Start Time","End Date","End Time","Duration (h)","Duration (decimal)","Billable Rate (USD)","Billable Amount (USD)"
"Project A","","","Peter Tester","","peter.test@email.test","Development, Backend","No","03/04/2024","10:23:52 AM","03/04/2024","10:23:52 AM","00:00:00","0.00","0.00","0.00"
"Project B","Working hard","Task 1","Peter Tester","","peter.test@email.test","","Yes","03/04/2024","10:23 AM","03/04/2024","11:23:01 AM","01:00:01","0.00","0.00","0.00"
1 Project Description Task User Group Email Tags Billable Start Date Start Time End Date End Time Duration (h) Duration (decimal) Billable Rate (USD) Billable Amount (USD)
2 Project A Peter Tester peter.test@email.test Development, Backend No 03/04/2024 10:23:52 AM 03/04/2024 10:23:52 AM 00:00:00 0.00 0.00 0.00
3 Project B Working hard Task 1 Peter Tester peter.test@email.test Yes 03/04/2024 10:23 AM 03/04/2024 11:23:01 AM 01:00:01 0.00 0.00 0.00

View File

@@ -0,0 +1,2 @@
"Project","Description","Task","User","Group","Email","Tags","Billable","Start Date","Start Time","End Date","End Time","Client"
"Project A","Working hard","Task 1","Peter Tester","","peter.test@email.test","","Yes","03/04/2024","10:23 AM","03/04/2024","11:23:01 AM"
1 Project Description Task User Group Email Tags Billable Start Date Start Time End Date End Time Client
2 Project A Working hard Task 1 Peter Tester peter.test@email.test Yes 03/04/2024 10:23 AM 03/04/2024 11:23:01 AM

View File

@@ -96,6 +96,29 @@ class ClockifyProjectsImporterTest extends ImporterTestAbstract
); );
} }
public function test_import_of_test_file_without_client_column_succeeds(): void
{
// Arrange
$organization = Organization::factory()->create();
$timezone = 'Europe/Vienna';
$importer = new ClockifyProjectsImporter;
$importer->init($organization);
// Newer Clockify exports no longer contain a "Client" column.
$data = Storage::disk('testfiles')->get('clockify_projects_import_test_4.csv');
// Act
$importer->importData($data, $timezone);
// Assert
$project = Project::query()->where('organization_id', $organization->id)->where('name', 'Project Without Client Column')->firstOrFail();
$this->assertNull($project->client_id);
$this->assertSame(100 * 3600, $project->estimated_time);
$this->assertEqualsCanonicalizing(
['Task 1', 'Task 2'],
Task::query()->where('project_id', $project->id)->pluck('name')->all(),
);
}
public function test_import_supports_activities_column_alias_for_tasks(): void public function test_import_supports_activities_column_alias_for_tasks(): void
{ {
// Arrange // Arrange

View File

@@ -136,6 +136,46 @@ class ClockifyTimeEntriesImporterTest extends ImporterTestAbstract
$this->assertSame(1, $report->tasksCreated); $this->assertSame(1, $report->tasksCreated);
} }
public function test_import_of_test_file_without_client_column_succeeds(): void
{
// Arrange
$organization = Organization::factory()->create();
$timezone = 'Europe/Vienna';
$importer = new ClockifyTimeEntriesImporter;
$importer->init($organization);
// Newer Clockify exports no longer contain a "Client" column.
$data = Storage::disk('testfiles')->get('clockify_time_entries_import_test_6.csv');
// Act
$importer->importData($data, $timezone);
$report = $importer->getReport();
// Assert
$this->assertSame(2, $report->timeEntriesCreated);
$this->assertSame(2, $report->projectsCreated);
$this->assertSame(0, $report->clientsCreated);
}
public function test_import_of_test_file_with_client_column_but_missing_values_succeeds(): void
{
// Arrange
$organization = Organization::factory()->create();
$timezone = 'Europe/Vienna';
$importer = new ClockifyTimeEntriesImporter;
$importer->init($organization);
// Rows shorter than the header are padded with null by the CSV reader.
$data = Storage::disk('testfiles')->get('clockify_time_entries_import_test_7.csv');
// Act
$importer->importData($data, $timezone);
$report = $importer->getReport();
// Assert
$this->assertSame(1, $report->timeEntriesCreated);
$this->assertSame(1, $report->projectsCreated);
$this->assertSame(0, $report->clientsCreated);
}
public function test_import_fails_if_month_in_date_is_bigger_than_12(): void public function test_import_fails_if_month_in_date_is_bigger_than_12(): void
{ {
// Arrange // Arrange