From 83844a0c27de989fb8648c850f465c552daef5ab Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 14 Nov 2023 13:24:51 +1100 Subject: [PATCH 1/5] Task imports --- app/Import/Definitions/TaskMap.php | 66 +++++++++ app/Import/Providers/BaseImport.php | 88 ++++++++++++ app/Import/Providers/Csv.php | 30 +++++ .../Transformer/Csv/TaskTransformer.php | 126 ++++++++++++++++++ 4 files changed, 310 insertions(+) create mode 100644 app/Import/Definitions/TaskMap.php create mode 100644 app/Import/Transformer/Csv/TaskTransformer.php diff --git a/app/Import/Definitions/TaskMap.php b/app/Import/Definitions/TaskMap.php new file mode 100644 index 000000000000..d974fa51aea2 --- /dev/null +++ b/app/Import/Definitions/TaskMap.php @@ -0,0 +1,66 @@ + 'task.number', + 1 => 'task.user_id', + 2 => 'task.rate', + 3 => 'project.name', + 4 => 'client.name', + 5 => 'client.email', + 6 => 'task.description', + 7 => 'task.is_billable', + 8 => 'task.start_date', + 9 => 'task.end_date', + 10 => 'task.start_time', + 11 => 'task.end_time', + 12 => 'task.duration', + 13 => 'task.status', + 14 => 'task.custom_value1', + 15 => 'task.custom_value1', + 16 => 'task.custom_value1', + 17 => 'task.custom_value1', + 18 => 'task.notes', + ]; + } + + public static function import_keys() + { + return [ + 0 => 'texts.task_number', + 1 => 'texts.user', + 2 => 'texts.task_rate', + 3 => 'texts.project', + 4 => 'texts.client', + 5 => 'texts.client_email', + 6 => 'texts.description', + 7 => 'texts.billable', + 8 => 'texts.start_date', + 9 => 'texts.end_date', + 10 => 'texts.start_time', + 11 => 'texts.end_time', + 12 => 'texts.duration', + 13 => 'texts.status', + 14 => 'texts.task1', + 15 => 'texts.task2', + 16 => 'texts.task3', + 17 => 'texts.task4', + 18 => 'texts.notes', + ]; + } +} diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php index f5396e7efd6b..f934e59fde13 100644 --- a/app/Import/Providers/BaseImport.php +++ b/app/Import/Providers/BaseImport.php @@ -16,6 +16,7 @@ use App\Factory\InvoiceFactory; use App\Factory\PaymentFactory; use App\Factory\QuoteFactory; use App\Factory\RecurringInvoiceFactory; +use App\Factory\TaskFactory; use App\Http\Requests\Quote\StoreQuoteRequest; use App\Import\ImportException; use App\Jobs\Mail\NinjaMailerJob; @@ -30,6 +31,7 @@ use App\Repositories\InvoiceRepository; use App\Repositories\PaymentRepository; use App\Repositories\QuoteRepository; use App\Repositories\RecurringInvoiceRepository; +use App\Repositories\TaskRepository; use App\Utils\Traits\CleanLineItems; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; @@ -158,6 +160,32 @@ class BaseImport }, $csvData); } + private function groupTasks($csvData, $key) + { + + if (! $key) { + return $csvData; + } + + // Group by tasks. + $grouped = []; + + foreach ($csvData as $item) { + if (empty($item[$key])) { + $this->error_array['task'][] = [ + 'task' => $item, + 'error' => 'No task number', + ]; + } else { + $grouped[$item[$key]][] = $item; + } + } + + return $grouped; + + + } + private function groupInvoices($csvData, $key) { if (! $key) { @@ -413,6 +441,66 @@ class BaseImport return $count; } + public function ingestTasks($tasks, $task_number_key) + { + $count = 0; + + $task_transformer = $this->transformer; + + $task_repository = new TaskRepository(); + // $task_repository->import_mode = true; + + $tasks = $this->groupTasks($tasks, $task_number_key); + + foreach ($tasks as $raw_task) { + $task_data = []; + try { + $task_data = $task_transformer->transform($raw_task); + $task_data['user_id'] = $this->company->owner()->id; + + $validator = $this->request_name::runFormRequest($task_data); + + if ($validator->fails()) { + $this->error_array['task'][] = [ + 'invoice' => $task_data, + 'error' => $validator->errors()->all(), + ]; + } else { + $task = TaskFactory::create( + $this->company->id, + $this->company->owner()->id + ); + + $task_repository->save($task_data, $task); + + $count++; + + } + } catch (\Exception $ex) { + if (\DB::connection(config('database.default'))->transactionLevel() > 0) { + \DB::connection(config('database.default'))->rollBack(); + } + + if ($ex instanceof ImportException) { + $message = $ex->getMessage(); + } else { + report($ex); + $message = 'Unknown error '; + nlog($ex->getMessage()); + nlog($task_data); + } + + $this->error_array['task'][] = [ + 'task' => $task_data, + 'error' => $message, + ]; + } + } + + return $count; + } + + public function ingestInvoices($invoices, $invoice_number_key) { diff --git a/app/Import/Providers/Csv.php b/app/Import/Providers/Csv.php index 9e29d5af9f41..97444ddb6f92 100644 --- a/app/Import/Providers/Csv.php +++ b/app/Import/Providers/Csv.php @@ -19,6 +19,7 @@ use App\Factory\PaymentFactory; use App\Factory\ProductFactory; use App\Factory\QuoteFactory; use App\Factory\RecurringInvoiceFactory; +use App\Factory\TaskFactory; use App\Factory\VendorFactory; use App\Http\Requests\BankTransaction\StoreBankTransactionRequest; use App\Http\Requests\Client\StoreClientRequest; @@ -28,6 +29,7 @@ use App\Http\Requests\Payment\StorePaymentRequest; use App\Http\Requests\Product\StoreProductRequest; use App\Http\Requests\Quote\StoreQuoteRequest; use App\Http\Requests\RecurringInvoice\StoreRecurringInvoiceRequest; +use App\Http\Requests\Task\StoreTaskRequest; use App\Http\Requests\Vendor\StoreVendorRequest; use App\Import\Transformer\Bank\BankTransformer; use App\Import\Transformer\Csv\ClientTransformer; @@ -37,6 +39,7 @@ use App\Import\Transformer\Csv\PaymentTransformer; use App\Import\Transformer\Csv\ProductTransformer; use App\Import\Transformer\Csv\QuoteTransformer; use App\Import\Transformer\Csv\RecurringInvoiceTransformer; +use App\Import\Transformer\Csv\TaskTransformer; use App\Import\Transformer\Csv\VendorTransformer; use App\Repositories\BankTransactionRepository; use App\Repositories\ClientRepository; @@ -46,6 +49,7 @@ use App\Repositories\PaymentRepository; use App\Repositories\ProductRepository; use App\Repositories\QuoteRepository; use App\Repositories\RecurringInvoiceRepository; +use App\Repositories\TaskRepository; use App\Repositories\VendorRepository; use App\Services\Bank\BankMatchingService; use App\Utils\Traits\MakesHash; @@ -69,6 +73,7 @@ class Csv extends BaseImport implements ImportInterface 'quote', 'bank_transaction', 'recurring_invoice', + 'tasks', ]) ) { $this->{$entity}(); @@ -348,6 +353,31 @@ class Csv extends BaseImport implements ImportInterface public function task() { + $entity_type = 'task'; + + $data = $this->getCsvData($entity_type); + + if (is_array($data)) { + $data = $this->preTransformCsv($data, $entity_type); + } + + if (empty($data)) { + $this->entity_count['invoices'] = 0; + return; + } + + $this->request_name = StoreTaskRequest::class; + $this->repository_name = TaskRepository::class; + $this->factory_name = TaskFactory::class; + + $this->repository = app()->make($this->repository_name); + // $this->repository->import_mode = true; + + $this->transformer = new TaskTransformer($this->company); + + $task_count = $this->ingestTasks($data, 'task.number'); + + $this->entity_count['tasks'] = $task_count; } public function transform(array $data) diff --git a/app/Import/Transformer/Csv/TaskTransformer.php b/app/Import/Transformer/Csv/TaskTransformer.php new file mode 100644 index 000000000000..060e5b04b0ce --- /dev/null +++ b/app/Import/Transformer/Csv/TaskTransformer.php @@ -0,0 +1,126 @@ +stubbed_timestamp = time(); + + $task_data = reset($task_items_data); + + $clientId = $this->getClient( + $this->getString($task_data, 'client.name'), + $this->getString($task_data, 'client.email') + ); + + $transformed = [ + 'company_id' => $this->company->id, + 'number' => $this->getString($task_data, 'task.number'), + 'user_id' => $this->getString($task_data, 'task.user_id'), + 'client_id' => $clientId, + 'project_id' => $this->getProjectId($task_data['project.name'], $clientId), + 'description' => $this->getString($task_data, 'task.description'), + 'status' => $this->getTaskStatusId($task_data), + 'custom_value1' => $this->getString($task_data, 'task.custom_value1'), + 'custom_value2' => $this->getString($task_data, 'task.custom_value2'), + 'custom_value3' => $this->getString($task_data, 'task.custom_value3'), + 'custom_value4' => $this->getString($task_data, 'task.custom_value4'), + ]; + + $time_log = collect($task_items_data) + ->map(function ($item) { + + return $this->parseLog($item); + + })->toJson(); + + nlog($time_log); + + $transformed['time_log'] = $time_log; + + return $transformed; + } + + private function parseLog($item): array + { + $start_date = false; + $end_date = false; + + $notes = $item['task.notes'] ?? ''; + $is_billable = $item['task.is_billable'] ?? false; + + if(isset($item['start_date']) && + isset($item['end_date'])) { + $start_date = $this->resolveStartDate($item); + $end_date = $this->resolveEndDate($item); + } elseif(isset($item['duration'])) { + $duration = strtotime($item['duration']) - strtotime('TODAY'); + $start_date = $this->stubbed_timestamp; + $end_date = $this->stubbed_timestamp + $duration; + $this->stubbed_timestamp++; + } else { + return []; + } + + return [$start_date, $end_date, $notes, $is_billable]; + } + + private function resolveStartDate($item) + { + + $stub_start_date = $item['start_date'] . ' ' . isset($item['start_time']) ?? ''; + + try { + $stub_start_date = \Carbon\Carbon::parse($stub_start_date); + $this->stubbed_timestamp = $stub_start_date->timestamp; + return $stub_start_date->timestamp; + } catch (\Exception $e) { + return $this->stubbed_timestamp; + } + + } + + private function resolveEndDate($item) + { + + $stub_start_date = $item['end_date'] . ' ' . isset($item['end_time']) ?? ''; + + try { + $stub_start_date = \Carbon\Carbon::parse($stub_start_date); + + if($stub_start_date->timestamp == $this->stubbed_timestamp) { + $this->stubbed_timestamp++; + return $this->stubbed_timestamp; + } + + $this->stubbed_timestamp = $stub_start_date->timestamp++; + return $stub_start_date->timestamp; + } catch (\Exception $e) { + return $this->stubbed_timestamp++; + } + + } + +} From 57a2a836f1005631719f1d9ba0921d09d8d41756 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 14 Nov 2023 14:53:25 +1100 Subject: [PATCH 2/5] Task imports + tests --- app/Import/Providers/BaseImport.php | 7 +- app/Import/Providers/Csv.php | 6 +- app/Import/Transformer/BaseTransformer.php | 3 + .../Transformer/Csv/TaskTransformer.php | 82 +++++++-- tests/Feature/Import/CSV/TaskImportTest.php | 168 ++++++++++++++++++ tests/Feature/Import/tasks.csv | 48 +++++ tests/Feature/Import/tasks2.csv | 48 +++++ 7 files changed, 337 insertions(+), 25 deletions(-) create mode 100644 tests/Feature/Import/CSV/TaskImportTest.php create mode 100644 tests/Feature/Import/tasks.csv create mode 100644 tests/Feature/Import/tasks2.csv diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php index f934e59fde13..0ad36b20474a 100644 --- a/app/Import/Providers/BaseImport.php +++ b/app/Import/Providers/BaseImport.php @@ -163,7 +163,7 @@ class BaseImport private function groupTasks($csvData, $key) { - if (! $key) { + if (! $key || count(array_column($csvData, $key)) == 0) { return $csvData; } @@ -448,10 +448,9 @@ class BaseImport $task_transformer = $this->transformer; $task_repository = new TaskRepository(); - // $task_repository->import_mode = true; $tasks = $this->groupTasks($tasks, $task_number_key); - + foreach ($tasks as $raw_task) { $task_data = []; try { @@ -496,7 +495,7 @@ class BaseImport ]; } } - +nlog($count); return $count; } diff --git a/app/Import/Providers/Csv.php b/app/Import/Providers/Csv.php index 97444ddb6f92..63b19aa2f044 100644 --- a/app/Import/Providers/Csv.php +++ b/app/Import/Providers/Csv.php @@ -73,7 +73,7 @@ class Csv extends BaseImport implements ImportInterface 'quote', 'bank_transaction', 'recurring_invoice', - 'tasks', + 'task', ]) ) { $this->{$entity}(); @@ -362,7 +362,7 @@ class Csv extends BaseImport implements ImportInterface } if (empty($data)) { - $this->entity_count['invoices'] = 0; + $this->entity_count['tasks'] = 0; return; } @@ -378,6 +378,8 @@ class Csv extends BaseImport implements ImportInterface $task_count = $this->ingestTasks($data, 'task.number'); $this->entity_count['tasks'] = $task_count; + + } public function transform(array $data) diff --git a/app/Import/Transformer/BaseTransformer.php b/app/Import/Transformer/BaseTransformer.php index 31c05ceb16a5..c1f1d43cbdf5 100644 --- a/app/Import/Transformer/BaseTransformer.php +++ b/app/Import/Transformer/BaseTransformer.php @@ -676,6 +676,9 @@ class BaseTransformer */ public function getProjectId($name, $clientId = null) { + if(strlen($name) == 0) + return null; + $project = Project::query()->where('company_id', $this->company->id) ->where('is_deleted', false) ->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [ diff --git a/app/Import/Transformer/Csv/TaskTransformer.php b/app/Import/Transformer/Csv/TaskTransformer.php index 060e5b04b0ce..9f817ab04ad4 100644 --- a/app/Import/Transformer/Csv/TaskTransformer.php +++ b/app/Import/Transformer/Csv/TaskTransformer.php @@ -11,6 +11,7 @@ namespace App\Import\Transformer\Csv; +use App\Models\TaskStatus; use App\Import\Transformer\BaseTransformer; /** @@ -35,12 +36,14 @@ class TaskTransformer extends BaseTransformer $this->getString($task_data, 'client.email') ); + $projectId = $task_data['project.name'] ?? ''; + $transformed = [ 'company_id' => $this->company->id, 'number' => $this->getString($task_data, 'task.number'), 'user_id' => $this->getString($task_data, 'task.user_id'), 'client_id' => $clientId, - 'project_id' => $this->getProjectId($task_data['project.name'], $clientId), + 'project_id' => $this->getProjectId($projectId, $clientId), 'description' => $this->getString($task_data, 'task.description'), 'status' => $this->getTaskStatusId($task_data), 'custom_value1' => $this->getString($task_data, 'task.custom_value1'), @@ -49,6 +52,11 @@ class TaskTransformer extends BaseTransformer 'custom_value4' => $this->getString($task_data, 'task.custom_value4'), ]; + if(count($task_items_data) == count($task_items_data, COUNT_RECURSIVE)) { + $transformed['time_log'] = json_encode([$this->parseLog($task_items_data)]); + return $transformed; + } + $time_log = collect($task_items_data) ->map(function ($item) { @@ -56,32 +64,36 @@ class TaskTransformer extends BaseTransformer })->toJson(); - nlog($time_log); - $transformed['time_log'] = $time_log; return $transformed; } - private function parseLog($item): array + private function parseLog($item) { $start_date = false; $end_date = false; $notes = $item['task.notes'] ?? ''; - $is_billable = $item['task.is_billable'] ?? false; + + if(isset($item['task.is_billable']) && is_string($item['task.is_billable']) && in_array($item['task.is_billable'], ['yes', 'true', '1'])) + $is_billable = true; + elseif(isset($item['task.is_billable']) && is_bool($item['task.is_billable'])) + $is_billable = $item['task.is_billable']; + else + $is_billable = false; - if(isset($item['start_date']) && - isset($item['end_date'])) { + if(isset($item['task.start_date']) && + isset($item['task.end_date'])) { $start_date = $this->resolveStartDate($item); $end_date = $this->resolveEndDate($item); - } elseif(isset($item['duration'])) { - $duration = strtotime($item['duration']) - strtotime('TODAY'); + } elseif(isset($item['task.duration'])) { + $duration = strtotime($item['task.duration']) - strtotime('TODAY'); $start_date = $this->stubbed_timestamp; $end_date = $this->stubbed_timestamp + $duration; - $this->stubbed_timestamp++; + $this->stubbed_timestamp; } else { - return []; + return ''; } return [$start_date, $end_date, $notes, $is_billable]; @@ -90,13 +102,17 @@ class TaskTransformer extends BaseTransformer private function resolveStartDate($item) { - $stub_start_date = $item['start_date'] . ' ' . isset($item['start_time']) ?? ''; + $stub_start_date = $item['task.start_date']; + $stub_start_date .= isset($item['task.start_time']) ? " ".$item['task.start_time'] : ''; try { + $stub_start_date = \Carbon\Carbon::parse($stub_start_date); $this->stubbed_timestamp = $stub_start_date->timestamp; + return $stub_start_date->timestamp; } catch (\Exception $e) { + nlog($e->getMessage()); return $this->stubbed_timestamp; } @@ -105,22 +121,50 @@ class TaskTransformer extends BaseTransformer private function resolveEndDate($item) { - $stub_start_date = $item['end_date'] . ' ' . isset($item['end_time']) ?? ''; + $stub_end_date = $item['task.end_date']; + $stub_end_date .= isset($item['task.end_time']) ? " ".$item['task.end_time'] : ''; try { - $stub_start_date = \Carbon\Carbon::parse($stub_start_date); - if($stub_start_date->timestamp == $this->stubbed_timestamp) { - $this->stubbed_timestamp++; + $stub_end_date = \Carbon\Carbon::parse($stub_end_date); + + if($stub_end_date->timestamp == $this->stubbed_timestamp) { + $this->stubbed_timestamp; return $this->stubbed_timestamp; } - $this->stubbed_timestamp = $stub_start_date->timestamp++; - return $stub_start_date->timestamp; + $this->stubbed_timestamp = $stub_end_date->timestamp; + return $stub_end_date->timestamp; } catch (\Exception $e) { - return $this->stubbed_timestamp++; + nlog($e->getMessage()); + + return $this->stubbed_timestamp; } } + private function getTaskStatusId($item): ?int + { + if(isset($item['task.status'])) + { + $name = strtolower(trim($item['task.status'])); + + $ts = TaskStatus::query()->where('company_id', $this->company->id) + ->where('is_deleted', false) + ->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [ + strtolower(str_replace(' ', '', $name)), + ]) + ->first(); + + if($ts) + return $ts->id; + } + + return TaskStatus::where('company_id', $this->company->id) + ->where('is_deleted', false) + ->orderBy('status_order', 'asc') + ->first()->id ?? null; + + } + } diff --git a/tests/Feature/Import/CSV/TaskImportTest.php b/tests/Feature/Import/CSV/TaskImportTest.php new file mode 100644 index 000000000000..4ebfaaa6cc61 --- /dev/null +++ b/tests/Feature/Import/CSV/TaskImportTest.php @@ -0,0 +1,168 @@ +withoutMiddleware(ThrottleRequests::class); + + config(['database.default' => config('ninja.db.default')]); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + + auth()->login($this->user); + } + + public function testTaskImportWithGroupedTaskNumbers() + { + Task::query() + ->where('company_id', $this->company->id) + ->forceDelete(); + + $this->assertEquals(0, Task::withTrashed()->where('company_id', $this->company->id)->count()); + + /*Need to import clients first*/ + $csv = file_get_contents( + base_path().'/tests/Feature/Import/tasks2.csv' + ); + $hash = Str::random(32); + $column_map = [ + 0 => 'task.user_id', + 3 => 'project.name', + 2 => 'client.name', + 4 => 'task.number', + 5 => 'task.description', + 6 => 'task.is_billable', + 7 => 'task.start_date', + 9 => 'task.end_date', + 8 => 'task.start_time', + 10 => 'task.end_time', + 11 => 'task.duration', + ]; + + $data = [ + 'hash' => $hash, + 'column_map' => ['task' => ['mapping' => $column_map]], + 'skip_header' => true, + 'import_type' => 'csv', + ]; + + Cache::put($hash.'-task', base64_encode($csv), 360); + + $csv_importer = new Csv($data, $this->company); + + $this->assertInstanceOf(Csv::class, $csv_importer); + + $csv_importer->import('task'); + + $base_transformer = new BaseTransformer($this->company); + + $task = Task::where('company_id', $this->company->id)->where('number', 'x1234')->first(); + $this->assertNotNull($task); + $this->assertEquals(1998, $task->calcDuration()); + + $time_log = json_decode($task->time_log); + + foreach($time_log as $log) { + $this->assertFalse($log[3]); + } + + $task = Task::where('company_id', $this->company->id)->where('number', 'x1233')->first(); + $this->assertNotNull($task); + $this->assertEquals(9833, $task->calcDuration()); + + $time_log = json_decode($task->time_log); + + foreach($time_log as $log) + { + $this->assertFalse($log[3]); + } + + + } + + + + public function testTaskImport() + { + Task::query() + ->where('company_id', $this->company->id) + ->forceDelete(); + + $this->assertEquals(0, Task::withTrashed()->where('company_id', $this->company->id)->count()); + + /*Need to import clients first*/ + $csv = file_get_contents( + base_path().'/tests/Feature/Import/tasks.csv' + ); + $hash = Str::random(32); + $column_map = [ + 0 => 'task.user_id', + 3 => 'project.name', + 2 => 'client.name', + 5 => 'task.description', + 6 => 'task.is_billable', + 7 => 'task.start_date', + 9 => 'task.end_date', + 8 => 'task.start_time', + 10 => 'task.end_time', + 11 => 'task.duration', + ]; + + $data = [ + 'hash' => $hash, + 'column_map' => ['task' => ['mapping' => $column_map]], + 'skip_header' => true, + 'import_type' => 'csv', + ]; + + Cache::put($hash.'-task', base64_encode($csv), 360); + + $csv_importer = new Csv($data, $this->company); + + $this->assertInstanceOf(Csv::class, $csv_importer); + + $csv_importer->import('task'); + + $base_transformer = new BaseTransformer($this->company); + + } + + +} \ No newline at end of file diff --git a/tests/Feature/Import/tasks.csv b/tests/Feature/Import/tasks.csv new file mode 100644 index 000000000000..c0c6f3c2b393 --- /dev/null +++ b/tests/Feature/Import/tasks.csv @@ -0,0 +1,48 @@ +User,Email,Client,Project,Task,Description,Billable,Start date,Start time,End date,End time,Duration,Tags,Amount () +Jimmy,user@example.com,,Fixup My Code,,Short and sweet,No,2023-10-23,10:01:07,2023-10-23,12:15:02,02:13:55,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-23,13:07:17,2023-10-23,14:11:17,01:04:00,, +Jimmy,user@example.com,,,,Side Hustle,No,2023-10-23,14:11:19,2023-10-23,17:08:38,02:57:19,, +Bob,bob@example.com,,Fixup My Code,,,No,2023-10-23,14:49:00,2023-10-23,18:03:43,03:14:43,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-23,17:31:40,2023-10-23,18:18:56,00:47:16,, +Bob,bob@example.com,,,,,No,2023-10-24,10:00:00,2023-10-24,17:05:00,07:05:00,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,10:01:00,2023-10-24,10:35:55,00:34:55,, +Jimmy,user@example.com,,Gov Proj,,euro handball training,No,2023-10-24,10:35:55,2023-10-24,10:39:49,00:03:54,, +Jimmy,user@example.com,,Gov Proj,,Fix all the servers,No,2023-10-24,10:39:50,2023-10-24,11:05:25,00:25:35,, +Jimmy,user@example.com,,Gov Proj,,euro handball training,No,2023-10-24,11:05:27,2023-10-24,11:10:56,00:05:29,, +Jimmy,user@example.com,,Gov Proj,,clean the house,No,2023-10-24,11:11:08,2023-10-24,11:17:11,00:06:03,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,11:17:13,2023-10-24,11:21:36,00:04:23,, +Jimmy,user@example.com,,Gov Proj,,euro handball training,No,2023-10-24,11:21:42,2023-10-24,11:22:58,00:01:16,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,11:22:59,2023-10-24,11:32:55,00:09:56,, +Jimmy,user@example.com,,Gov Proj,,wax the floor,No,2023-10-24,11:32:55,2023-10-24,12:19:17,00:46:22,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,12:19:19,2023-10-24,12:40:57,00:21:38,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,13:18:54,2023-10-24,17:05:55,03:47:01,, +Jimmy,user@example.com,,Gov Proj,,Sand the car,No,2023-10-24,17:05:59,2023-10-24,17:08:27,00:02:28,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,17:08:29,2023-10-24,18:00:33,00:52:04,, +Bob,bob@example.com,,Fixup My Code,,,No,2023-10-25,09:58:45,2023-10-25,10:32:09,00:33:24,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-25,10:09:06,2023-10-25,13:01:49,02:52:43,, +Bob,bob@example.com,,Gov Proj,,Attack the windows,No,2023-10-25,10:32:10,2023-10-25,15:54:19,05:22:09,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-25,13:52:30,2023-10-25,15:30:48,01:38:18,, +Jimmy,user@example.com,,Gov Proj,,assist old lady to cross the street,No,2023-10-25,15:30:48,2023-10-25,15:49:10,00:18:22,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-25,15:49:11,2023-10-25,18:09:43,02:20:32,, +Bob,bob@example.com,,Fixup My Code,,,No,2023-10-25,16:08:29,2023-10-25,18:06:12,01:57:43,, +Jlecrenay,jlecrenay@solyme.com,,Gestion interne,,admin work,No,2023-10-26,09:24:00,2023-10-26,12:22:00,02:58:00,, +Bob,bob@example.com,,,,admin work,No,2023-10-26,09:25:49,2023-10-26,11:58:34,02:32:45,, +Jimmy,user@example.com,,,,,No,2023-10-26,09:26:22,2023-10-26,19:04:06,09:37:44,, +Bob,bob@example.com,,Fixup My Code,,administration,No,2023-10-26,13:25:23,2023-10-26,17:08:00,03:42:37,, +Jlecrenay,jlecrenay@solyme.com,,Holiday Project,,training,No,2023-10-26,13:37:31,2023-10-26,18:14:00,04:36:29,, +Jlecrenay,jlecrenay@solyme.com,,,,reading,No,2023-10-27,08:37:17,2023-10-27,09:24:45,00:47:28,, +Jlecrenay,jlecrenay@solyme.com,,LOCATION,,travel,No,2023-10-27,09:25:29,2023-10-27,09:41:46,00:16:17,, +Jlecrenay,jlecrenay@solyme.com,,,,travel,No,2023-10-27,09:42:04,2023-10-27,10:46:35,01:04:31,, +Jimmy,user@example.com,,Fixup My Code,,travel,No,2023-10-27,09:55:02,2023-10-27,10:25:52,00:30:50,, +Bob,bob@example.com,,Fixup My Code,,travel,No,2023-10-27,10:10:00,2023-10-27,10:44:00,00:34:00,, +Jimmy,user@example.com,,Gov Proj,,travel,No,2023-10-27,10:25:53,2023-10-27,10:41:22,00:15:29,, +Jimmy,user@example.com,,Gov Proj,,travel,No,2023-10-27,10:41:23,2023-10-27,10:59:56,00:18:33,, +Bob,bob@example.com,,Fixup My Code,,,No,2023-10-27,10:44:00,2023-10-27,10:59:00,00:15:00,, +Jlecrenay,jlecrenay@solyme.com,,Holiday Project,,travel,No,2023-10-27,10:46:38,2023-10-27,12:20:13,01:33:35,, +Jimmy,user@example.com,,Gov Proj,,travel,No,2023-10-27,11:01:42,2023-10-27,11:06:01,00:04:19,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-27,11:09:38,2023-10-27,12:20:07,01:10:29,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-27,13:02:18,2023-10-27,13:57:17,00:54:59,, +Jlecrenay,jlecrenay@solyme.com,,Covert Ops,,Holiday,No,2023-10-27,13:20:27,2023-10-27,18:10:58,04:50:31,, +Jimmy,user@example.com,,Gov Proj,,fix the wifi,No,2023-10-27,13:57:17,2023-10-27,14:39:11,00:41:54,, +Bob,bob@example.com,,Gov Proj,,fix the spark plugs,No,2023-10-27,14:29:25,2023-10-27,16:31:24,02:01:59,, +Jimmy,user@example.com,,BLOOM,,travel,No,2023-10-27,14:39:12,2023-10-27,15:12:30,00:33:18,, diff --git a/tests/Feature/Import/tasks2.csv b/tests/Feature/Import/tasks2.csv new file mode 100644 index 000000000000..15d3911eca3d --- /dev/null +++ b/tests/Feature/Import/tasks2.csv @@ -0,0 +1,48 @@ +User,Email,Client,Project,Task,Description,Billable,Start date,Start time,End date,End time,Duration,Tags,Amount () +Bob,user@example.com,,Fixup My Code,x111,Short and sweet,No,2023-10-23,10:01:07,2023-10-23,12:15:02,02:13:55,, +Bob,user@example.com,,Fixup My Code,x111,,No,2023-10-23,13:07:17,2023-10-23,14:11:17,01:04:00,, +Bob,user@example.com,,,,Side Hustle,No,2023-10-23,14:11:19,2023-10-23,17:08:38,02:57:19,, +James,james@example.com,,Fixup My Code,x111,,No,2023-10-23,14:49:00,2023-10-23,18:03:43,03:14:43,, +Bob,user@example.com,,Fixup My Code,x111,,No,2023-10-23,17:31:40,2023-10-23,18:18:56,00:47:16,, +James,james@example.com,,,,,No,2023-10-24,10:00:00,2023-10-24,17:05:00,07:05:00,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,10:01:00,2023-10-24,10:35:55,00:34:55,, +Bob,user@example.com,,Gov Proj,x112,euro handball training,No,2023-10-24,10:35:55,2023-10-24,10:39:49,00:03:54,, +Bob,user@example.com,,Gov Proj,x112,Fix all the servers,No,2023-10-24,10:39:50,2023-10-24,11:05:25,00:25:35,, +Bob,user@example.com,,Gov Proj,x112,euro handball training,No,2023-10-24,11:05:27,2023-10-24,11:10:56,00:05:29,, +Bob,user@example.com,,Gov Proj,x112,clean the house,No,2023-10-24,11:11:08,2023-10-24,11:17:11,00:06:03,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,11:17:13,2023-10-24,11:21:36,00:04:23,, +Bob,user@example.com,,Gov Proj,,euro handball training,No,2023-10-24,11:21:42,2023-10-24,11:22:58,00:01:16,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,11:22:59,2023-10-24,11:32:55,00:09:56,, +Bob,user@example.com,,Gov Proj,,wax the floor,No,2023-10-24,11:32:55,2023-10-24,12:19:17,00:46:22,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,12:19:19,2023-10-24,12:40:57,00:21:38,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,13:18:54,2023-10-24,17:05:55,03:47:01,, +Bob,user@example.com,,Gov Proj,,Sand the car,No,2023-10-24,17:05:59,2023-10-24,17:08:27,00:02:28,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,17:08:29,2023-10-24,18:00:33,00:52:04,, +James,james@example.com,,Fixup My Code,,,No,2023-10-25,09:58:45,2023-10-25,10:32:09,00:33:24,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-25,10:09:06,2023-10-25,13:01:49,02:52:43,, +James,james@example.com,,Gov Proj,,Attack the windows,No,2023-10-25,10:32:10,2023-10-25,15:54:19,05:22:09,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-25,13:52:30,2023-10-25,15:30:48,01:38:18,, +Bob,user@example.com,,Gov Proj,,assist old lady to cross the street,No,2023-10-25,15:30:48,2023-10-25,15:49:10,00:18:22,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-25,15:49:11,2023-10-25,18:09:43,02:20:32,, +James,james@example.com,,Fixup My Code,,,No,2023-10-25,16:08:29,2023-10-25,18:06:12,01:57:43,, +Jlecrenay,jlecrenay@solyme.com,,Gestion interne,,admin work,No,2023-10-26,09:24:00,2023-10-26,12:22:00,02:58:00,, +James,james@example.com,,,,admin work,No,2023-10-26,09:25:49,2023-10-26,11:58:34,02:32:45,, +Bob,user@example.com,,,,,No,2023-10-26,09:26:22,2023-10-26,19:04:06,09:37:44,, +James,james@example.com,,Fixup My Code,,administration,No,2023-10-26,13:25:23,2023-10-26,17:08:00,03:42:37,, +Jlecrenay,jlecrenay@solyme.com,,Holiday Project,,training,No,2023-10-26,13:37:31,2023-10-26,18:14:00,04:36:29,, +Jlecrenay,jlecrenay@solyme.com,,,,reading,No,2023-10-27,08:37:17,2023-10-27,09:24:45,00:47:28,, +Jlecrenay,jlecrenay@solyme.com,,LOCATION,,travel,No,2023-10-27,09:25:29,2023-10-27,09:41:46,00:16:17,, +Jlecrenay,jlecrenay@solyme.com,,,,travel,No,2023-10-27,09:42:04,2023-10-27,10:46:35,01:04:31,, +Bob,user@example.com,,Fixup My Code,,travel,No,2023-10-27,09:55:02,2023-10-27,10:25:52,00:30:50,, +James,james@example.com,,Fixup My Code,,travel,No,2023-10-27,10:10:00,2023-10-27,10:44:00,00:34:00,, +Bob,user@example.com,,Gov Proj,,travel,No,2023-10-27,10:25:53,2023-10-27,10:41:22,00:15:29,, +Bob,user@example.com,,Gov Proj,,travel,No,2023-10-27,10:41:23,2023-10-27,10:59:56,00:18:33,, +James,james@example.com,,Fixup My Code,,,No,2023-10-27,10:44:00,2023-10-27,10:59:00,00:15:00,, +Jlecrenay,jlecrenay@solyme.com,,Holiday Project,,travel,No,2023-10-27,10:46:38,2023-10-27,12:20:13,01:33:35,, +Bob,user@example.com,,Gov Proj,,travel,No,2023-10-27,11:01:42,2023-10-27,11:06:01,00:04:19,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-27,11:09:38,2023-10-27,12:20:07,01:10:29,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-27,13:02:18,2023-10-27,13:57:17,00:54:59,, +Jlecrenay,jlecrenay@solyme.com,,Covert Ops,,Holiday,No,2023-10-27,13:20:27,2023-10-27,18:10:58,04:50:31,, +Bob,user@example.com,,Gov Proj,x1233,fix the wifi,No,2023-10-27,13:57:17,2023-10-27,14:39:11,00:41:54,, +James,james@example.com,,Gov Proj,x1233,fix the spark plugs,No,2023-10-27,14:29:25,2023-10-27,16:31:24,02:01:59,, +Bob,user@example.com,,BLOOM,x1234,travel,No,2023-10-27,14:39:12,2023-10-27,15:12:30,00:33:18,, From d254ff533618e724d4b899e7b5a037c6f1daea96 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 14 Nov 2023 19:58:28 +1100 Subject: [PATCH 3/5] Prevent Template Service from running over statements due to long data parsing --- app/Services/Client/Statement.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php index ded85ac4b47f..556baf76f84f 100644 --- a/app/Services/Client/Statement.php +++ b/app/Services/Client/Statement.php @@ -100,13 +100,13 @@ class Statement ], \App\Services\PdfMaker\Design::STATEMENT), 'variables' => $variables, 'options' => [ - 'client' => $this->client, - 'entity' => $this->entity, - 'variables' => $variables, - 'invoices' => $this->getInvoices()->cursor(), - 'payments' => $this->getPayments()->cursor(), - 'credits' => $this->getCredits()->cursor(), - 'aging' => $this->getAging(), + // 'client' => $this->client, + // 'entity' => $this->entity, + // 'variables' => $variables, + // 'invoices' => $this->getInvoices()->cursor(), + // 'payments' => $this->getPayments()->cursor(), + // 'credits' => $this->getCredits()->cursor(), + // 'aging' => $this->getAging(), ], 'process_markdown' => $this->entity->client->company->markdown_enabled, ]; From 1662e0467171904090296c97b017561c237d2285 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 15 Nov 2023 08:53:07 +1100 Subject: [PATCH 4/5] Fixes for column types in designer --- app/Services/Pdf/PdfBuilder.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php index dd453097e073..9b19192e1bcf 100644 --- a/app/Services/Pdf/PdfBuilder.php +++ b/app/Services/Pdf/PdfBuilder.php @@ -844,10 +844,12 @@ class PdfBuilder public function processTaxColumns(string $type): void { if ($type == 'product') { + $column_type = 'product'; $type_id = 1; } if ($type == 'task') { + $column_type = 'task'; $type_id = 2; } @@ -886,10 +888,10 @@ class PdfBuilder array_push($taxes, sprintf('%s%s.tax_rate3', '$', $type)); } - $key = array_search(sprintf('%s%s.tax', '$', $type), $this->service->config->pdf_variables["{$type}_columns"], true); + $key = array_search(sprintf('%s%s.tax', '$', $type), $this->service->config->pdf_variables["{$column_type}_columns"], true); if ($key !== false) { - array_splice($this->service->config->pdf_variables["{$type}_columns"], $key, 1, $taxes); + array_splice($this->service->config->pdf_variables["{$column_type}_columns"], $key, 1, $taxes); } } } From 03a3a8a921529bd0c84f8ec4f867f05b07228480 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 15 Nov 2023 08:57:27 +1100 Subject: [PATCH 5/5] V5.7.49 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 13f1af3e031e..de824ef2c595 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.7.48 \ No newline at end of file +5.7.49 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index ba7c433f033d..2728bc291033 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION','5.7.48'), - 'app_tag' => env('APP_TAG','5.7.48'), + 'app_version' => env('APP_VERSION','5.7.49'), + 'app_tag' => env('APP_TAG','5.7.49'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false),