diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index ab2873a965..d11c9ef72c 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -20,6 +20,7 @@ use App\Models\Payment; use League\Fractal\Manager; use Illuminate\Support\Carbon; use App\Utils\Traits\MakesHash; +use App\Transformers\TaskTransformer; use App\Transformers\PaymentTransformer; use Illuminate\Database\Eloquent\Builder; use League\Fractal\Serializer\ArraySerializer; @@ -348,8 +349,6 @@ class BaseExport 'custom_value4' => 'task.custom_value4', 'status' => 'task.status_id', 'project' => 'task.project_id', - 'invoice' => 'task.invoice_id', - 'client' => 'task.client_id', ]; protected function filterByClients($query) @@ -385,6 +384,7 @@ class BaseExport 'quote' => $value = $this->resolveQuoteKey($parts[1], $entity, $transformer), 'purchase_order' => $value = $this->resolvePurchaseOrderKey($parts[1], $entity, $transformer), 'payment' => $value = $this->resolvePaymentKey($parts[1], $entity, $transformer), + 'task' => $value = $this->resolveTaskKey($parts[1], $entity, $transformer), default => $value = '' }; @@ -450,6 +450,22 @@ class BaseExport } + private function resolveTaskKey($column, $entity, $transformer) + { + nlog("searching for {$column}"); + + $transformed_entity = $transformer->transform($entity); + + if(array_key_exists($column, $transformed_entity)) { + return $transformed_entity[$column]; + } + + return ''; + + } + + + private function resolveVendorKey($column, $entity, $transformer) { @@ -587,7 +603,23 @@ class BaseExport } - $transformed_invoice = $transformer->transform($entity); + if($transformer instanceof TaskTransformer) { + $transformed_invoice = $transformer->includeInvoice($entity); + + if(!$transformed_invoice) + return ''; + + $manager = new Manager(); + $manager->setSerializer(new ArraySerializer()); + $transformed_invoice = $manager->createData($transformed_invoice)->toArray(); + + } + + if(array_key_exists($column, $transformed_invoice)) { + return $transformed_invoice[$column]; + } elseif (array_key_exists(str_replace("invoice.", "", $column), $transformed_invoice)) { + return $transformed_invoice[$column]; + } if($column == 'status') return $entity->stringStatus($entity->status_id); @@ -886,6 +918,8 @@ class BaseExport } } + // nlog($header); + return $header; } } diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index fa60698e47..387bda9020 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -46,9 +46,7 @@ class TaskExport extends BaseExport 'custom_value4' => 'custom_value4', 'status' => 'status_id', 'project' => 'project_id', - 'invoice' => 'invoice_id', - 'client' => 'client_id', - ]; + ]; private array $decorate_keys = [ 'status', @@ -109,38 +107,39 @@ class TaskExport extends BaseExport $entity = []; $transformed_entity = $this->entity_transformer->transform($task); + foreach (array_values($this->input['report_keys']) as $key) { + $keyval = array_search($key, $this->entity_keys); + + if(!$keyval) { + $keyval = array_search(str_replace("task.", "", $key), $this->entity_keys) ?? $key; + } + + if(!$keyval) { + $keyval = $key; + } + + if (array_key_exists($key, $transformed_entity)) { + $entity[$keyval] = $transformed_entity[$key]; + } elseif (array_key_exists($keyval, $transformed_entity)) { + $entity[$keyval] = $transformed_entity[$keyval]; + } + else { + $entity[$keyval] = $this->resolveKey($keyval, $task, $this->entity_transformer); + } + } + + $entity['start_date'] = ''; + $entity['end_date'] = ''; + $entity['duration'] = ''; + + if (is_null($task->time_log) || (is_array(json_decode($task->time_log, 1)) && count(json_decode($task->time_log, 1)) == 0)) { - foreach (array_values($this->input['report_keys']) as $key) { - $keyval = array_search($key, $this->entity_keys); - - if (array_key_exists($key, $transformed_entity)) { - $entity[$keyval] = $transformed_entity[$key]; - } else { - $entity[$keyval] = ''; - } - } - - $entity['start_date'] = ''; - $entity['end_date'] = ''; - $entity['duration'] = ''; - - $entity = $this->decorateAdvancedFields($task, $entity); - - ksort($entity); $this->csv->insertOne($entity); - } elseif (is_array(json_decode($task->time_log, 1)) && count(json_decode($task->time_log, 1)) > 0) { - foreach (array_values($this->input['report_keys']) as $key) { - $keyval = array_search($key, $this->entity_keys); - - if (array_key_exists($key, $transformed_entity)) { - $entity[$keyval] = $transformed_entity[$key]; - } else { - $entity[$keyval] = ''; - } - } - + } else { $this->iterateLogs($task, $entity); } + + } private function iterateLogs(Task $task, array $entity) @@ -163,39 +162,26 @@ class TaskExport extends BaseExport } foreach ($logs as $key => $item) { - if (in_array('start_date', $this->input['report_keys'])) { + if (in_array('task.start_date', $this->input['report_keys']) || in_array('start_date', $this->input['report_keys'])) { $entity['start_date'] = Carbon::createFromTimeStamp($item[0])->setTimezone($timezone_name)->format($date_format_default); } - if (in_array('end_date', $this->input['report_keys']) && $item[1] > 0) { + if ((in_array('task.end_date', $this->input['report_keys']) || in_array('end_date', $this->input['report_keys'])) && $item[1] > 0) { $entity['end_date'] = Carbon::createFromTimeStamp($item[1])->setTimezone($timezone_name)->format($date_format_default); } - if (in_array('end_date', $this->input['report_keys']) && $item[1] == 0) { + if ((in_array('task.end_date', $this->input['report_keys']) || in_array('end_date', $this->input['report_keys'])) && $item[1] == 0) { $entity['end_date'] = ctrans('texts.is_running'); } - if (in_array('duration', $this->input['report_keys'])) { + if (in_array('task.duration', $this->input['report_keys']) || in_array('duration', $this->input['report_keys'])) { $entity['duration'] = $task->calcDuration(); } - - if (! array_key_exists('duration', $entity)) { - $entity['duration'] = ''; - } - - if (! array_key_exists('start_date', $entity)) { - $entity['start_date'] = ''; - } - - if (! array_key_exists('end_date', $entity)) { - $entity['end_date'] = ''; - } - + $entity = $this->decorateAdvancedFields($task, $entity); - - ksort($entity); + $this->csv->insertOne($entity); - + unset($entity['start_date']); unset($entity['end_date']); unset($entity['duration']); @@ -212,14 +198,6 @@ class TaskExport extends BaseExport $entity['project'] = $task->project()->exists() ? $task->project->name : ''; } - if (in_array('client_id', $this->input['report_keys'])) { - $entity['client'] = $task->client ? $task->client->present()->name() : ''; - } - - if (in_array('invoice_id', $this->input['report_keys'])) { - $entity['invoice'] = $task->invoice ? $task->invoice->number : ''; - } - return $entity; } } diff --git a/app/Transformers/TaskTransformer.php b/app/Transformers/TaskTransformer.php index 028e2bf44e..d2080b6a45 100644 --- a/app/Transformers/TaskTransformer.php +++ b/app/Transformers/TaskTransformer.php @@ -37,6 +37,7 @@ class TaskTransformer extends EntityTransformer 'status', 'project', 'user', + 'invoice', ]; public function includeDocuments(Task $task) @@ -46,6 +47,17 @@ class TaskTransformer extends EntityTransformer return $this->includeCollection($task->documents, $transformer, Document::class); } + public function includeInvoice(Task $task): ?Item + { + $transformer = new InvoiceTransformer($this->serializer); + + if (!$task->user) { + return null; + } + + return $this->includeItem($task->invoice, $transformer, Invoice::class); + } + public function includeUser(Task $task): ?Item { $transformer = new UserTransformer($this->serializer); diff --git a/phpstan.neon b/phpstan.neon index cebc91d327..b5b004a232 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,7 +5,7 @@ parameters: treatPhpDocTypesAsCertain: false parallel: jobSize: 5 - maximumNumberOfProcesses: 16 + maximumNumberOfProcesses: 1 processTimeout: 600.0 ignoreErrors: - '#Call to an undefined method .*badMethod\(\)#' diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index c46ee9eb5c..c8f5eb79e2 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -164,9 +164,92 @@ class ReportCsvGenerationTest extends TestCase } + + public function testTaskCustomColumnsCsvGeneration() + { + + + $invoice = \App\Models\Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'date' => '2023-01-01', + 'amount' => 1000, + 'balance' => 1000, + 'number' => '123456', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '12345', + 'public_notes' => 'Public5', + 'private_notes' => 'Private5', + 'terms' => 'Terms5', + ]); + + + $log = '[[1689547165,1689550765,"sumtin",true]]'; + + \App\Models\Task::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'invoice_id' => $invoice->id, + 'description' => 'test1', + 'time_log' => $log, + 'custom_value1' => 'Custom 11', + 'custom_value2' => 'Custom 22', + 'custom_value3' => 'Custom 33', + 'custom_value4' => 'Custom 44', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [ + 'client.name', + 'invoice.number', + 'invoice.amount', + 'task.start_date', + 'task.end_date', + 'task.duration', + 'task.description', + 'task.custom_value1', + 'task.custom_value2', + 'task.custom_value3', + 'task.custom_value4', + ], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/tasks', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals(3600, $this->getFirstValueByColumn($csv, 'Task Duration')); + $this->assertEquals('test1', $this->getFirstValueByColumn($csv, 'Task Description')); + $this->assertEquals('16/Jul/2023', $this->getFirstValueByColumn($csv, 'Task Start Date')); + $this->assertEquals('16/Jul/2023', $this->getFirstValueByColumn($csv, 'Task End Date')); + $this->assertEquals('Custom 11', $this->getFirstValueByColumn($csv, 'Task Custom Value 1')); + $this->assertEquals('Custom 22', $this->getFirstValueByColumn($csv, 'Task Custom Value 2')); + $this->assertEquals('Custom 33', $this->getFirstValueByColumn($csv, 'Task Custom Value 3')); + $this->assertEquals('Custom 44', $this->getFirstValueByColumn($csv, 'Task Custom Value 4')); + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('123456', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals(1000, $this->getFirstValueByColumn($csv, 'Invoice Amount')); + + } + + + + public function testTasksCsvGeneration() { + \App\Models\Task::query()->cursor()->each(function ($t) { + $t->forceDelete(); + }); + $log = '[[1689547165,1689550765,"sumtin",true]]'; \App\Models\Task::factory()->create([ @@ -247,13 +330,70 @@ class ReportCsvGenerationTest extends TestCase public function testPaymentCsvGeneration() + { + + $invoice = \App\Models\Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'date' => '2023-01-01', + 'amount' => 100, + 'balance' => 100, + 'number' => '12345', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $invoice->client->balance = 100; + $invoice->client->paid_to_date = 0; + $invoice->push(); + + $invoice->service()->markPaid()->save(); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [ + "payment.date", + "payment.amount", + "invoice.number", + "invoice.amount", + "client.name", + "client.balance", + "client.paid_to_date" + ], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/payments', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals(now()->addSeconds($this->company->timezone()->utc_offset)->format('Y-m-d'), $this->getFirstValueByColumn($csv, 'Payment Date')); + $this->assertEquals('12345', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Invoice Amount')); + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals(0, $this->getFirstValueByColumn($csv, 'Client Balance')); + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Client Paid to Date')); + + } + + + public function testPaymentCustomFieldsCsvGeneration() { \App\Models\Payment::factory()->create([ 'amount' => 500, 'date' => '2020-01-01', - 'company_id' => $this->company->id, 'user_id' => $this->user->id, + 'company_id' => $this->company->id, 'client_id' => $this->client->id, 'transaction_reference' => '1234', ]); @@ -279,6 +419,7 @@ class ReportCsvGenerationTest extends TestCase } + public function testClientCsvGeneration() { @@ -443,8 +584,6 @@ class ReportCsvGenerationTest extends TestCase $csv = $response->streamedContent(); - nlog($csv); - $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Recurring Invoice Invoice Number')); $this->assertEquals('Daily', $this->getFirstValueByColumn($csv, 'Recurring Invoice How Often')); @@ -694,8 +833,6 @@ class ReportCsvGenerationTest extends TestCase $csv = $response->streamedContent(); - nlog($csv); - $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Vendor Name')); $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Purchase Order Number')); $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Quantity'));