From 8515b350be94af0a4c3893ad96f795b3b86fc749 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 16 Jul 2023 17:42:34 +1000 Subject: [PATCH 01/32] Adjustments for restoring a deleted invoice with a deleted payment --- app/Repositories/InvoiceRepository.php | 4 + app/Services/Invoice/HandleRestore.php | 1 + tests/Feature/PaymentV2Test.php | 195 ++++++++++++++++++++++++- 3 files changed, 194 insertions(+), 6 deletions(-) diff --git a/app/Repositories/InvoiceRepository.php b/app/Repositories/InvoiceRepository.php index 83b6980f1567..c7347da552b0 100644 --- a/app/Repositories/InvoiceRepository.php +++ b/app/Repositories/InvoiceRepository.php @@ -97,6 +97,10 @@ class InvoiceRepository extends BaseRepository // reversed delete invoice actions $invoice = $invoice->service()->handleRestore()->save(); + /* If the reverse did not succeed due to rules, then do not restore / unarchive */ + if($invoice->is_deleted) + return $invoice; + parent::restore($invoice); return $invoice; diff --git a/app/Services/Invoice/HandleRestore.php b/app/Services/Invoice/HandleRestore.php index c3e2947e8732..d5bf6c8275ba 100644 --- a/app/Services/Invoice/HandleRestore.php +++ b/app/Services/Invoice/HandleRestore.php @@ -44,6 +44,7 @@ class HandleRestore extends AbstractService //cannot restore an invoice with a deleted payment foreach ($this->invoice->payments as $payment) { if (($this->invoice->paid_to_date == 0) && $payment->is_deleted) { + $this->invoice->delete(); return $this->invoice; } } diff --git a/tests/Feature/PaymentV2Test.php b/tests/Feature/PaymentV2Test.php index 761c00b03361..11f448b3b481 100644 --- a/tests/Feature/PaymentV2Test.php +++ b/tests/Feature/PaymentV2Test.php @@ -11,20 +11,21 @@ namespace Tests\Feature; +use Tests\TestCase; use App\Models\Client; -use App\Models\ClientContact; use App\Models\Credit; use App\Models\Invoice; use App\Models\Payment; +use Tests\MockAccountData; +use App\Models\ClientContact; use App\Utils\Traits\MakesHash; +use App\Factory\InvoiceItemFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Testing\DatabaseTransactions; -use Illuminate\Foundation\Testing\WithoutEvents; -use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Support\Facades\Session; use Illuminate\Validation\ValidationException; -use Tests\MockAccountData; -use Tests\TestCase; +use Illuminate\Foundation\Testing\WithoutEvents; +use Illuminate\Routing\Middleware\ThrottleRequests; +use Illuminate\Foundation\Testing\DatabaseTransactions; /** * @test @@ -189,4 +190,186 @@ class PaymentV2Test extends TestCase $this->assertEquals(20, $client->fresh()->paid_to_date); } + +public function testStorePaymentWithCreditsThenDeletingInvoicesAndThenPayments() + { + $client = Client::factory()->create(['company_id' =>$this->company->id, 'user_id' => $this->user->id, 'balance' => 100, 'paid_to_date' => 0]); + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + ]); + + $line_items = []; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 100; + + $line_items[] = $item; + + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'status_id' => Invoice::STATUS_SENT, + 'uses_inclusive_taxes' => false, + 'amount' => 100, + 'balance' => 100, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => $line_items + ]); + + $this->assertEquals(100, $client->balance); + $this->assertEquals(0, $client->paid_to_date); + $this->assertEquals(100, $invoice->amount); + $this->assertEquals(100, $invoice->balance); + + $credit = Credit::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'status_id' => Invoice::STATUS_SENT, + 'uses_inclusive_taxes' => false, + 'amount' => 20, + 'balance' => 20, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => [] + ]); + + $this->assertEquals(20, $credit->amount); + $this->assertEquals(20, $credit->balance); + + $data = [ + 'client_id' => $client->hashed_id, + 'invoices' => [ + [ + 'invoice_id' => $invoice->hashed_id, + 'amount' => 100, + ], + ], + 'credits' => [ + [ + 'credit_id' => $credit->hashed_id, + 'amount' => 20, + ], + ], + 'date' => '2020/12/12', + + ]; + + $response = null; + + try { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/payments?include=invoices', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + $this->assertNotNull($message); + } + + $arr = $response->json(); + $response->assertStatus(200); + + $payment_id = $arr['data']['id']; + + $payment = Payment::find($this->decodePrimaryKey($payment_id)); + + $this->assertNotNull($payment); + $this->assertNotNull($payment->invoices()); + $this->assertEquals(1, $payment->invoices()->count()); + $this->assertEquals(80, $payment->amount); + $this->assertEquals(0, $client->fresh()->balance); + $this->assertEquals(100, $client->fresh()->paid_to_date); + + $invoice = $invoice->fresh(); + + //delete the invoice + + $data = [ + 'action' => 'delete', + 'ids' => [ + $invoice->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/bulk', $data); + + $response->assertStatus(200); + + $payment = $payment->fresh(); + $invoice = $invoice->fresh(); + + $this->assertTrue($invoice->is_deleted); + $this->assertFalse($payment->is_deleted); + + $data = [ + 'action' => 'delete', + 'ids' => [ + $payment->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/payments/bulk', $data); + + $payment = $payment->fresh(); + $this->assertTrue($payment->is_deleted); + + $data = [ + 'action' => 'restore', + 'ids' => [ + $invoice->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/bulk', $data); + + $response->assertStatus(200); + $invoice = $invoice->fresh(); + + $this->assertTrue($invoice->is_deleted); + $this->assertTrue($invoice->trashed()); + + // $this->assertEquals(0, $payment->amount); + // $this->assertEquals(0, $client->fresh()->balance); + // $this->assertEquals(0, $client->fresh()->paid_to_date); + + // $data = [ + // 'action' => 'restore', + // 'ids' => [ + // $invoice->hashed_id, + // ], + // ]; + + // $response = $this->withHeaders([ + // 'X-API-SECRET' => config('ninja.api_secret'), + // 'X-API-TOKEN' => $this->token, + // ])->post('/api/v1/invoices/bulk', $data); + + // $invoice = $invoice->fresh(); + // $this->assertEquals(false, $invoice->is_deleted); + + // $payment = $payment->fresh(); + + // $this->assertEquals(0, $payment->amount); + // $this->assertEquals(20, $client->fresh()->paid_to_date); + + } + } \ No newline at end of file From c858dcf40d64c0b901693471be8052a767548c06 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 16 Jul 2023 18:56:20 +1000 Subject: [PATCH 02/32] Adjustments for restoring a deleted invoice with a deleted payment --- tests/Feature/PaymentV2Test.php | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/tests/Feature/PaymentV2Test.php b/tests/Feature/PaymentV2Test.php index 11f448b3b481..5f1f386ef173 100644 --- a/tests/Feature/PaymentV2Test.php +++ b/tests/Feature/PaymentV2Test.php @@ -281,6 +281,7 @@ public function testStorePaymentWithCreditsThenDeletingInvoicesAndThenPayments() $payment_id = $arr['data']['id']; $payment = Payment::find($this->decodePrimaryKey($payment_id)); + $credit = $credit->fresh(); $this->assertNotNull($payment); $this->assertNotNull($payment->invoices()); @@ -288,9 +289,10 @@ public function testStorePaymentWithCreditsThenDeletingInvoicesAndThenPayments() $this->assertEquals(80, $payment->amount); $this->assertEquals(0, $client->fresh()->balance); $this->assertEquals(100, $client->fresh()->paid_to_date); + $this->assertEquals(0, $credit->balance); $invoice = $invoice->fresh(); - + //delete the invoice $data = [ @@ -346,29 +348,13 @@ public function testStorePaymentWithCreditsThenDeletingInvoicesAndThenPayments() $this->assertTrue($invoice->is_deleted); $this->assertTrue($invoice->trashed()); - // $this->assertEquals(0, $payment->amount); - // $this->assertEquals(0, $client->fresh()->balance); - // $this->assertEquals(0, $client->fresh()->paid_to_date); + $client = $client->fresh(); + $credit = $credit->fresh(); - // $data = [ - // 'action' => 'restore', - // 'ids' => [ - // $invoice->hashed_id, - // ], - // ]; - - // $response = $this->withHeaders([ - // 'X-API-SECRET' => config('ninja.api_secret'), - // 'X-API-TOKEN' => $this->token, - // ])->post('/api/v1/invoices/bulk', $data); - - // $invoice = $invoice->fresh(); - // $this->assertEquals(false, $invoice->is_deleted); - - // $payment = $payment->fresh(); - - // $this->assertEquals(0, $payment->amount); - // $this->assertEquals(20, $client->fresh()->paid_to_date); + $this->assertEquals(0, $client->balance); + $this->assertEquals(0, $client->paid_to_date); + // $this->assertEquals(20, $client->credit_balance); + $this->assertEquals(20, $credit->balance); } From 323064b2bad8c1708fa9599f725a266d8e2aa80b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 16 Jul 2023 20:05:30 +1000 Subject: [PATCH 03/32] Tests for report generation --- .../Export/ReportCsvGenerationTest.php | 133 +++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 918a8e78d8b7..80d608560185 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -26,6 +26,7 @@ use App\Utils\Traits\MakesHash; use App\DataMapper\CompanySettings; use App\Factory\CompanyUserFactory; use App\Factory\InvoiceItemFactory; +use App\Models\Expense; use App\Services\Report\ARDetailReport; use Illuminate\Routing\Middleware\ThrottleRequests; @@ -227,6 +228,17 @@ class ReportCsvGenerationTest extends TestCase } + private function getFirstValueByColumn($csv, $column) + { + $reader = Reader::createFromString($csv); + $reader->setHeaderOffset(0); + + $res = $reader->fetchColumnByName($column); + $res = iterator_to_array($res, true); + + return $res[1]; + } + public function testCreditCsvGeneration() { @@ -256,6 +268,125 @@ class ReportCsvGenerationTest extends TestCase ])->post('/api/v1/reports/credits', $data); $response->assertStatus(200); - + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'PO Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); + } + + public function testInvoiceCsvGeneration() + { + + Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'date' => '2020-01-01', + 'due_date' => '2021-01-02', + 'partial_due_date' => '2021-01-03', + 'partial' => 10, + 'discount' => 10, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + 'footer' => 'Footer', + 'tax_name1' => 'Tax 1', + 'tax_rate1' => 10, + 'tax_name2' => 'Tax 2', + 'tax_rate2' => 20, + 'tax_name3' => 'Tax 3', + 'tax_rate3' => 30, + + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/invoices', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'PO Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Date')); + $this->assertEquals('2021-01-02', $this->getFirstValueByColumn($csv, 'Due Date')); + $this->assertEquals('2021-01-03', $this->getFirstValueByColumn($csv, 'Partial Due Date')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Partial/Deposit')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + $this->assertEquals('Footer', $this->getFirstValueByColumn($csv, 'Footer')); + $this->assertEquals('Tax 1', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + $this->assertEquals('Tax 2', $this->getFirstValueByColumn($csv, 'Tax Name 2')); + $this->assertEquals('20', $this->getFirstValueByColumn($csv, 'Tax Rate 2')); + $this->assertEquals('Tax 3', $this->getFirstValueByColumn($csv, 'Tax Name 3')); + $this->assertEquals('30', $this->getFirstValueByColumn($csv, 'Tax Rate 3')); + $this->assertEquals('Sent', $this->getFirstValueByColumn($csv, 'Status')); + + } + + + public function testExpenseCsvGeneration() + { + Expense::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'public_notes' => 'Public', + 'private_notes' => 'Private', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/expenses', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals($this->user->present()->name(), $this->getFirstValueByColumn($csv, 'User')); + + } } \ No newline at end of file From a3febc93887bbe435894052e601440a190ab7274 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 16 Jul 2023 20:17:43 +1000 Subject: [PATCH 04/32] Adjustments for Quote Exports --- app/Export/CSV/BaseExport.php | 19 ++++- app/Export/CSV/QuoteExport.php | 23 ++++-- app/Models/Quote.php | 2 +- .../Export/ReportCsvGenerationTest.php | 75 +++++++++++++++++++ 4 files changed, 110 insertions(+), 9 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index ccbf10b45057..0a6e4c96dfd4 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -193,13 +193,13 @@ class BaseExport ]; protected array $quote_report_keys = [ - "quote_number" => "quote.number", + "number" => "quote.number", "amount" => "quote.amount", "balance" => "quote.balance", "paid_to_date" => "quote.paid_to_date", "po_number" => "quote.po_number", "date" => "quote.date", - "due_date" => "quote.due_date", + "valid_until" => "quote.due_date", "terms" => "quote.terms", "footer" => "quote.footer", "status" => "quote.status", @@ -347,6 +347,7 @@ class BaseExport 'vendor' => $value = $this->resolveVendorKey($parts[1], $entity, $transformer), 'vendor_contact' => $value = $this->resolveVendorContactKey($parts[1], $entity, $transformer), 'invoice' => $value = $this->resolveInvoiceKey($parts[1], $entity, $transformer), + '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), default => $value = '' @@ -511,6 +512,20 @@ class BaseExport return ''; } + private function resolveQuoteKey($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 resolveInvoiceKey($column, $entity, $transformer) { nlog("searching for {$column}"); diff --git a/app/Export/CSV/QuoteExport.php b/app/Export/CSV/QuoteExport.php index 8031f01f05e2..5711e46067b5 100644 --- a/app/Export/CSV/QuoteExport.php +++ b/app/Export/CSV/QuoteExport.php @@ -43,7 +43,7 @@ class QuoteExport extends BaseExport 'custom_value4' => 'custom_value4', 'date' => 'date', 'discount' => 'discount', - 'due_date' => 'due_date', + 'valid_until' => 'due_date', 'exchange_rate' => 'exchange_rate', 'footer' => 'footer', 'number' => 'number', @@ -115,17 +115,28 @@ class QuoteExport extends BaseExport private function buildRow(Quote $quote) :array { - $transformed_quote = $this->quote_transformer->transform($quote); + $transformed_entity = $this->quote_transformer->transform($quote); $entity = []; foreach (array_values($this->input['report_keys']) as $key) { $keyval = array_search($key, $this->entity_keys); - if (array_key_exists($key, $transformed_quote)) { - $entity[$keyval] = $transformed_quote[$key]; - } else { - $entity[$keyval] = ''; + if(!$keyval) { + $keyval = array_search(str_replace("invoice.", "", $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, $quote, $this->quote_transformer); } } diff --git a/app/Models/Quote.php b/app/Models/Quote.php index ae0d4b2333d2..04c3cee2cd94 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -455,7 +455,7 @@ class Quote extends BaseModel case self::STATUS_DRAFT: return ctrans('texts.draft'); case self::STATUS_SENT: - return ctrans('texts.pending'); + return ctrans('texts.sent'); case self::STATUS_APPROVED: return ctrans('texts.approved'); case self::STATUS_EXPIRED: diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 80d608560185..1ff8fbba5fed 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -355,6 +355,81 @@ class ReportCsvGenerationTest extends TestCase } +public function testQuoteCsvGeneration() + { + + \App\Models\Quote::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'date' => '2020-01-01', + 'due_date' => '2020-01-01', + 'partial_due_date' => '2021-01-03', + 'partial' => 10, + 'discount' => 10, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + 'footer' => 'Footer', + 'tax_name1' => 'Tax 1', + 'tax_rate1' => 10, + 'tax_name2' => 'Tax 2', + 'tax_rate2' => 20, + 'tax_name3' => 'Tax 3', + 'tax_rate3' => 30, + + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/quotes', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'PO Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Date')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Valid Until')); + $this->assertEquals('2021-01-03', $this->getFirstValueByColumn($csv, 'Partial Due Date')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Partial/Deposit')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + $this->assertEquals('Footer', $this->getFirstValueByColumn($csv, 'Footer')); + $this->assertEquals('Tax 1', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + $this->assertEquals('Tax 2', $this->getFirstValueByColumn($csv, 'Tax Name 2')); + $this->assertEquals('20', $this->getFirstValueByColumn($csv, 'Tax Rate 2')); + $this->assertEquals('Tax 3', $this->getFirstValueByColumn($csv, 'Tax Name 3')); + $this->assertEquals('30', $this->getFirstValueByColumn($csv, 'Tax Rate 3')); + $this->assertEquals('Expired', $this->getFirstValueByColumn($csv, 'Status')); + + } + public function testExpenseCsvGeneration() { From 6bbf3dface8f455f02cd5550c2c8fdf8acd9fa15 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 16 Jul 2023 20:34:31 +1000 Subject: [PATCH 05/32] Updates for exchange rate validation --- app/Export/CSV/QuoteItemExport.php | 53 ++++++++++++++----- app/Factory/CreditFactory.php | 2 +- app/Factory/PurchaseOrderFactory.php | 3 +- app/Factory/QuoteFactory.php | 1 + .../Requests/Credit/StoreCreditRequest.php | 9 +++- .../Requests/Credit/UpdateCreditRequest.php | 7 ++- .../Requests/Invoice/StoreInvoiceRequest.php | 3 +- .../Requests/Invoice/UpdateInvoiceRequest.php | 7 ++- .../StorePurchaseOrderRequest.php | 7 ++- .../UpdatePurchaseOrderRequest.php | 5 ++ app/Http/Requests/Quote/StoreQuoteRequest.php | 5 ++ .../Requests/Quote/UpdateQuoteRequest.php | 5 ++ .../StoreRecurringInvoiceRequest.php | 7 ++- .../UpdateRecurringInvoiceRequest.php | 7 ++- 14 files changed, 99 insertions(+), 22 deletions(-) diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index 4059174a8af8..06cd71cc97b2 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -135,25 +135,44 @@ class QuoteItemExport extends BaseExport $transformed_items = []; - foreach ($quote->line_items as $item) { - $item_array = []; + $transformed_items = []; - foreach (array_values($this->input['report_keys']) as $key) { - if (str_contains($key, 'item.')) { - $key = str_replace('item.', '', $key); - $item_array[$key] = $item->{$key}; + foreach ($quote->line_items as $item) { + $item_array = []; + + foreach (array_values($this->input['report_keys']) as $key) { //items iterator produces item array + + if (str_contains($key, "item.")) { + + $key = str_replace("item.", "", $key); + + $keyval = $key; + + $keyval = str_replace("custom_value", "quote", $key); + + if($key == 'type_id') + $keyval = 'type'; + + if($key == 'tax_id') + $keyval = 'tax_category'; + + if (property_exists($item, $key)) { + $item_array[$keyval] = $item->{$key}; + } else { + $item_array[$keyval] = ''; + } } } $entity = []; - foreach (array_values($this->input['report_keys']) as $key) { - $keyval = array_search($key, $this->entity_keys); + foreach (array_values($this->input['report_keys']) as $key) { //create an array of report keys only + $keyval = array_search($key, $this->entity_keys); if (array_key_exists($key, $transformed_items)) { $entity[$keyval] = $transformed_items[$key]; } else { - $entity[$keyval] = ''; + $entity[$keyval] = ""; } } @@ -173,16 +192,26 @@ class QuoteItemExport extends BaseExport foreach (array_values($this->input['report_keys']) as $key) { $keyval = array_search($key, $this->entity_keys); + if(!$keyval) { + $keyval = array_search(str_replace("quote.", "", $key), $this->entity_keys) ?? $key; + } + + if(!$keyval) { + $keyval = $key; + } + if (array_key_exists($key, $transformed_quote)) { $entity[$keyval] = $transformed_quote[$key]; - } else { - $entity[$keyval] = ''; + } elseif (array_key_exists($keyval, $transformed_quote)) { + $entity[$keyval] = $transformed_quote[$keyval]; + } + else { + $entity[$keyval] = $this->resolveKey($keyval, $quote, $this->quote_transformer); } } return $this->decorateAdvancedFields($quote, $entity); } - private function decorateAdvancedFields(Quote $quote, array $entity) :array { if (in_array('currency_id', $this->input['report_keys'])) { diff --git a/app/Factory/CreditFactory.php b/app/Factory/CreditFactory.php index bb3643d469d2..ec1d8341a39b 100644 --- a/app/Factory/CreditFactory.php +++ b/app/Factory/CreditFactory.php @@ -49,7 +49,7 @@ class CreditFactory $credit->user_id = $user_id; $credit->company_id = $company_id; $credit->recurring_id = null; - + $credit->exchange_rate = 1; return $credit; } } diff --git a/app/Factory/PurchaseOrderFactory.php b/app/Factory/PurchaseOrderFactory.php index 31a082ac83b7..4e68f9ad2f0f 100644 --- a/app/Factory/PurchaseOrderFactory.php +++ b/app/Factory/PurchaseOrderFactory.php @@ -49,7 +49,8 @@ class PurchaseOrderFactory $purchase_order->user_id = $user_id; $purchase_order->company_id = $company_id; $purchase_order->recurring_id = null; - + $purchase_order->exchange_rate = 1; + return $purchase_order; } } diff --git a/app/Factory/QuoteFactory.php b/app/Factory/QuoteFactory.php index 64a4b42f244f..d312a6215646 100644 --- a/app/Factory/QuoteFactory.php +++ b/app/Factory/QuoteFactory.php @@ -46,6 +46,7 @@ class QuoteFactory $quote->user_id = $user_id; $quote->company_id = $company_id; $quote->paid_to_date = 0; + $quote->exchange_rate = 1; return $quote; } diff --git a/app/Http/Requests/Credit/StoreCreditRequest.php b/app/Http/Requests/Credit/StoreCreditRequest.php index 13eee9c59852..8616ab177bc0 100644 --- a/app/Http/Requests/Credit/StoreCreditRequest.php +++ b/app/Http/Requests/Credit/StoreCreditRequest.php @@ -67,7 +67,8 @@ class StoreCreditRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + if ($this->invoice_id) { $rules['invoice_id'] = new ValidInvoiceCreditRule(); } @@ -88,7 +89,11 @@ class StoreCreditRequest extends Request $input = $this->decodePrimaryKeys($input); $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; - //$input['line_items'] = json_encode($input['line_items']); + + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } } diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index 929ad49e443a..a397d41a8862 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -67,7 +67,8 @@ class UpdateCreditRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } @@ -81,6 +82,10 @@ class UpdateCreditRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $input['id'] = $this->credit->id; $this->replace($input); diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index 327aa8b96a04..37ff17758412 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -72,7 +72,8 @@ class StoreInvoiceRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 0a6992192e93..971de8712cd3 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -72,7 +72,8 @@ class UpdateInvoiceRequest extends Request $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; $rules['status_id'] = 'bail|sometimes|not_in:5'; //do not all cancelled invoices to be modfified. - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + // not needed. // $rules['partial_due_date'] = 'bail|sometimes|required_unless:partial,0,null'; @@ -95,6 +96,10 @@ class UpdateInvoiceRequest extends Request unset($input['documents']); } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index 91a312203354..71d8fe32de99 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -60,7 +60,8 @@ class StorePurchaseOrderRequest extends Request } $rules['status_id'] = 'nullable|integer|in:1,2,3,4,5'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } @@ -77,6 +78,10 @@ class StorePurchaseOrderRequest extends Request $input['amount'] = 0; $input['balance'] = 0; + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } } diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index e23fa91520b8..f772944ddd53 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -63,6 +63,7 @@ class UpdatePurchaseOrderRequest extends Request } $rules['status_id'] = 'sometimes|integer|in:1,2,3,4,5'; + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; return $rules; } @@ -79,6 +80,10 @@ class UpdatePurchaseOrderRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } } diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index 387f165de110..39e03dc37e33 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -55,6 +55,7 @@ class StoreQuoteRequest extends Request $rules['discount'] = 'sometimes|numeric'; $rules['is_amount_discount'] = ['boolean']; + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; // $rules['number'] = new UniqueQuoteNumberRule($this->all()); $rules['line_items'] = 'array'; @@ -72,6 +73,10 @@ class StoreQuoteRequest extends Request $input['amount'] = 0; $input['balance'] = 0; + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } } diff --git a/app/Http/Requests/Quote/UpdateQuoteRequest.php b/app/Http/Requests/Quote/UpdateQuoteRequest.php index 07b463936892..b692e82663d4 100644 --- a/app/Http/Requests/Quote/UpdateQuoteRequest.php +++ b/app/Http/Requests/Quote/UpdateQuoteRequest.php @@ -57,6 +57,7 @@ class UpdateQuoteRequest extends Request $rules['line_items'] = 'array'; $rules['discount'] = 'sometimes|numeric'; $rules['is_amount_discount'] = ['boolean']; + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; return $rules; } @@ -75,6 +76,10 @@ class UpdateQuoteRequest extends Request unset($input['documents']); } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $input['id'] = $this->quote->id; $this->replace($input); diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index 420afb7c09f7..11c266ac8f46 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -67,7 +67,8 @@ class StoreRecurringInvoiceRequest extends Request $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; $rules['due_date_days'] = 'bail|sometimes|string'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } @@ -143,6 +144,10 @@ class StoreRecurringInvoiceRequest extends Request unset($input['number']); } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index f268cbe84288..26cc4e2f44de 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -61,7 +61,8 @@ class UpdateRecurringInvoiceRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } @@ -121,6 +122,10 @@ class UpdateRecurringInvoiceRequest extends Request unset($input['documents']); } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } From 9bfaee5c1f8e60e9c1020c129821018d8b2e7597 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 16 Jul 2023 20:36:48 +1000 Subject: [PATCH 06/32] checks for importing exchange rates --- app/Import/Transformer/Csv/InvoiceTransformer.php | 2 +- app/Import/Transformer/Csv/QuoteTransformer.php | 2 +- app/Import/Transformer/Csv/RecurringInvoiceTransformer.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Import/Transformer/Csv/InvoiceTransformer.php b/app/Import/Transformer/Csv/InvoiceTransformer.php index 807a4c2eff43..1f9a18b289cc 100644 --- a/app/Import/Transformer/Csv/InvoiceTransformer.php +++ b/app/Import/Transformer/Csv/InvoiceTransformer.php @@ -114,7 +114,7 @@ class InvoiceTransformer extends BaseTransformer $invoice_data, 'invoice.custom_surcharge4' ), - 'exchange_rate' => $this->getString( + 'exchange_rate' => $this->getFloat( $invoice_data, 'invoice.exchange_rate' ), diff --git a/app/Import/Transformer/Csv/QuoteTransformer.php b/app/Import/Transformer/Csv/QuoteTransformer.php index a61014e3a22d..e1ab4d4a9344 100644 --- a/app/Import/Transformer/Csv/QuoteTransformer.php +++ b/app/Import/Transformer/Csv/QuoteTransformer.php @@ -114,7 +114,7 @@ class QuoteTransformer extends BaseTransformer $quote_data, 'quote.custom_surcharge4' ), - 'exchange_rate' => $this->getString( + 'exchange_rate' => $this->getFloat( $quote_data, 'quote.exchange_rate' ), diff --git a/app/Import/Transformer/Csv/RecurringInvoiceTransformer.php b/app/Import/Transformer/Csv/RecurringInvoiceTransformer.php index e19331d36151..d4c2a17bdffd 100644 --- a/app/Import/Transformer/Csv/RecurringInvoiceTransformer.php +++ b/app/Import/Transformer/Csv/RecurringInvoiceTransformer.php @@ -122,7 +122,7 @@ class RecurringInvoiceTransformer extends BaseTransformer $invoice_data, 'invoice.custom_surcharge4' ), - 'exchange_rate' => $this->getString( + 'exchange_rate' => $this->getFloat( $invoice_data, 'invoice.exchange_rate' ), From 6c3049f5cbb69fe1beba08ed037f436bc6329cb7 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 16 Jul 2023 20:39:41 +1000 Subject: [PATCH 07/32] checks for importing exchange rates --- app/Import/Transformer/BaseTransformer.php | 15 +++++++++++++++ app/Import/Transformer/Csv/InvoiceTransformer.php | 2 +- app/Import/Transformer/Csv/QuoteTransformer.php | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/Import/Transformer/BaseTransformer.php b/app/Import/Transformer/BaseTransformer.php index 0bdf3051decc..ddc190b029ed 100644 --- a/app/Import/Transformer/BaseTransformer.php +++ b/app/Import/Transformer/BaseTransformer.php @@ -319,6 +319,21 @@ class BaseTransformer // return Number::parseFloat($number); } + /** + * @param $data + * @param $field + * + * @return float + */ + public function getFloatOrOne($data, $field) + { + if (array_key_exists($field, $data)) + return Number::parseStringFloat($data[$field]) > 0 ? Number::parseStringFloat($data[$field]) : 1; + + return 1; + + } + /** * @param $name * diff --git a/app/Import/Transformer/Csv/InvoiceTransformer.php b/app/Import/Transformer/Csv/InvoiceTransformer.php index 1f9a18b289cc..1399990a2a9e 100644 --- a/app/Import/Transformer/Csv/InvoiceTransformer.php +++ b/app/Import/Transformer/Csv/InvoiceTransformer.php @@ -114,7 +114,7 @@ class InvoiceTransformer extends BaseTransformer $invoice_data, 'invoice.custom_surcharge4' ), - 'exchange_rate' => $this->getFloat( + 'exchange_rate' => $this->getFloatOrOne( $invoice_data, 'invoice.exchange_rate' ), diff --git a/app/Import/Transformer/Csv/QuoteTransformer.php b/app/Import/Transformer/Csv/QuoteTransformer.php index e1ab4d4a9344..93e24120d0bf 100644 --- a/app/Import/Transformer/Csv/QuoteTransformer.php +++ b/app/Import/Transformer/Csv/QuoteTransformer.php @@ -114,7 +114,7 @@ class QuoteTransformer extends BaseTransformer $quote_data, 'quote.custom_surcharge4' ), - 'exchange_rate' => $this->getFloat( + 'exchange_rate' => $this->getFloatOrOne( $quote_data, 'quote.exchange_rate' ), From b3b3180667bd19ad2599af3c63762c30d5d1f43f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 16 Jul 2023 20:51:21 +1000 Subject: [PATCH 08/32] Updates for quote item exports --- app/Export/CSV/BaseExport.php | 5 +++++ app/Export/CSV/QuoteItemExport.php | 12 +++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 0a6e4c96dfd4..1c299790d1cf 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -193,6 +193,10 @@ class BaseExport ]; protected array $quote_report_keys = [ + 'custom_value1' => 'quote.custom_value1', + 'custom_value2' => 'quote.custom_value2', + 'custom_value3' => 'quote.custom_value3', + 'custom_value4' => 'quote.custom_value4', "number" => "quote.number", "amount" => "quote.amount", "balance" => "quote.balance", @@ -817,6 +821,7 @@ class BaseExport $header[] = "{$prefix}" . ctrans("texts.{$key}"); } + // nlog($header); return $header; diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index 06cd71cc97b2..ffa5bb5a96de 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -77,11 +77,13 @@ class QuoteItemExport extends BaseExport 'tax_name2' => 'item.tax_name2', 'tax_name3' => 'item.tax_name3', 'line_total' => 'item.line_total', - // 'gross_line_total' => 'item.gross_line_total', - 'custom_value1' => 'item.custom_value1', - 'custom_value2' => 'item.custom_value2', - 'custom_value3' => 'item.custom_value3', - 'custom_value4' => 'item.custom_value4', + 'gross_line_total' => 'item.gross_line_total', + 'quote1' => 'item.custom_value1', + 'quote2' => 'item.custom_value2', + 'quote3' => 'item.custom_value3', + 'quote4' => 'item.custom_value4', + 'tax_category' => 'item.tax_id', + 'type' => 'item.type_id', ]; private array $decorate_keys = [ From 146559ada70ea06fe9211426b82e911fb862a0dc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Jul 2023 07:31:36 +1000 Subject: [PATCH 09/32] Update lock for lock invoice and add fees --- app/Jobs/Util/ReminderJob.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/Jobs/Util/ReminderJob.php b/app/Jobs/Util/ReminderJob.php index 80f2676d2ab5..1c10a0a3d5c4 100644 --- a/app/Jobs/Util/ReminderJob.php +++ b/app/Jobs/Util/ReminderJob.php @@ -129,12 +129,10 @@ class ReminderJob implements ShouldQueue $invoice->service()->touchReminder($reminder_template)->save(); $fees = $this->calcLateFee($invoice, $reminder_template); - if(in_array($invoice->client->getSetting('lock_invoices'), ['when_sent','when_paid'])) { + if($invoice->isLocked()) return $this->addFeeToNewInvoice($invoice, $reminder_template, $fees); - } - else - $invoice = $this->setLateFee($invoice, $fees[0], $fees[1]); - + + $invoice = $this->setLateFee($invoice, $fees[0], $fees[1]); //20-04-2022 fixes for endless reminders - generic template naming was wrong $enabled_reminder = 'enable_'.$reminder_template; From ee2a4894962215204482f6757d9ef745f48e49e0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Jul 2023 08:22:36 +1000 Subject: [PATCH 10/32] Tests for exports --- .../Export/ReportCsvGenerationTest.php | 114 +++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 1ff8fbba5fed..8900a94df743 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -161,6 +161,39 @@ class ReportCsvGenerationTest extends TestCase } + public function testPaymentCsvGeneration() + { + + \App\Models\Payment::factory()->create([ + 'amount' => 500, + 'date' => '2020-01-01', + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'transaction_reference' => '1234', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + '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(500, $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals(0, $this->getFirstValueByColumn($csv, 'Applied')); + $this->assertEquals(0, $this->getFirstValueByColumn($csv, 'Refunded')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Date')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Transaction Reference')); + + } + public function testClientCsvGeneration() { @@ -355,7 +388,83 @@ class ReportCsvGenerationTest extends TestCase } -public function testQuoteCsvGeneration() + public function testRecurringInvoiceCsvGeneration() + { + + \App\Models\RecurringInvoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'date' => '2020-01-01', + 'due_date' => '2021-01-02', + 'partial_due_date' => '2021-01-03', + 'partial' => 10, + 'discount' => 10, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + 'footer' => 'Footer', + 'tax_name1' => 'Tax 1', + 'tax_rate1' => 10, + 'tax_name2' => 'Tax 2', + 'tax_rate2' => 20, + 'tax_name3' => 'Tax 3', + 'tax_rate3' => 30, + 'frequency_id' => 1, + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/recurring_invoices', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'PO Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Date')); + $this->assertEquals('2021-01-02', $this->getFirstValueByColumn($csv, 'Due Date')); + $this->assertEquals('2021-01-03', $this->getFirstValueByColumn($csv, 'Partial Due Date')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Partial/Deposit')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + $this->assertEquals('Footer', $this->getFirstValueByColumn($csv, 'Footer')); + $this->assertEquals('Tax 1', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + $this->assertEquals('Tax 2', $this->getFirstValueByColumn($csv, 'Tax Name 2')); + $this->assertEquals('20', $this->getFirstValueByColumn($csv, 'Tax Rate 2')); + $this->assertEquals('Tax 3', $this->getFirstValueByColumn($csv, 'Tax Name 3')); + $this->assertEquals('30', $this->getFirstValueByColumn($csv, 'Tax Rate 3')); + $this->assertEquals('Daily', $this->getFirstValueByColumn($csv, 'Frequency')); + + } + + + public function testQuoteCsvGeneration() { \App\Models\Quote::factory()->create([ @@ -461,7 +570,8 @@ public function testQuoteCsvGeneration() $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); $this->assertEquals($this->user->present()->name(), $this->getFirstValueByColumn($csv, 'User')); - } + + } \ No newline at end of file From e68b45efcb5211d6493254c721a0853ae3a136ce Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Jul 2023 08:40:49 +1000 Subject: [PATCH 11/32] Task export tests --- .../Export/ReportCsvGenerationTest.php | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 8900a94df743..e980d459179a 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -161,6 +161,88 @@ class ReportCsvGenerationTest extends TestCase } + public function testTasksCsvGeneration() + { + + $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, + 'description' => 'test', + 'time_log' => $log, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + '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, 'Duration')); + $this->assertEquals('test', $this->getFirstValueByColumn($csv, 'Description')); + $this->assertEquals('16/Jul/2023', $this->getFirstValueByColumn($csv, 'Start Date')); + $this->assertEquals('16/Jul/2023', $this->getFirstValueByColumn($csv, 'End Date')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + + } + + public function testProductsCsvGeneration() + { + + \App\Models\Product::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'product_key' => 'product_key', + 'notes' => 'notes', + 'cost' => 100, + 'quantity' => 1, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/products', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('product_key', $this->getFirstValueByColumn($csv, 'Product')); + $this->assertEquals('notes', $this->getFirstValueByColumn($csv, 'Notes')); + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Cost')); + $this->assertEquals(1, $this->getFirstValueByColumn($csv, 'Quantity')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + + } + + public function testPaymentCsvGeneration() { From e0807a3a6a30ff9555fd235179a8a8014f9b60fa Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Jul 2023 09:47:14 +1000 Subject: [PATCH 12/32] Minor adjustments for blobs --- app/Http/Controllers/ClientPortal/InvoiceController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 6e04f4a7e938..2e432e964210 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -86,7 +86,7 @@ class InvoiceController extends Controller public function showBlob($hash) { - $data = Cache::pull($hash); + $data = Cache::get($hash); match($data['entity_type']){ 'invoice' => $invitation = InvoiceInvitation::withTrashed()->find($data['invitation_id']), From 18ca64c72c87a30181614b353dd7b01dd93edeff Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Jul 2023 10:22:53 +1000 Subject: [PATCH 13/32] CSVs with custom columns --- app/Export/CSV/ClientExport.php | 4 +-- .../Export/ReportCsvGenerationTest.php | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/Export/CSV/ClientExport.php b/app/Export/CSV/ClientExport.php index bcf93bf5897f..7d645c78b410 100644 --- a/app/Export/CSV/ClientExport.php +++ b/app/Export/CSV/ClientExport.php @@ -172,8 +172,8 @@ class ClientExport extends BaseExport $entity['shipping_country'] = $client->shipping_country ? ctrans("texts.country_{$client->shipping_country->name}") : ''; } - if (in_array('client.currency', $this->input['report_keys'])) { - $entity['currency'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code; + if (in_array('client.currency_id', $this->input['report_keys'])) { + $entity['client.currency_id'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code; } if (in_array('client.industry_id', $this->input['report_keys'])) { diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index e980d459179a..3f074cf922f1 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -143,10 +143,13 @@ class ReportCsvGenerationTest extends TestCase $this->client = Client::factory()->create([ 'user_id' => $this->user->id, + // 'assigned_user_id', $this->user->id, 'company_id' => $this->company->id, 'is_deleted' => 0, 'name' => 'bob', - 'address1' => '1234' + 'address1' => '1234', + 'balance' => 100, + 'paid_to_date' => 50, ]); ClientContact::factory()->create([ @@ -307,6 +310,35 @@ class ReportCsvGenerationTest extends TestCase } + public function testClientCustomColumnsCsvGeneration() + { + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","client.user","client.assigned_user","client.balance","client.paid_to_date","client.currency_id"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/clients', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Name')); + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals(50, $this->getFirstValueByColumn($csv, 'Paid to Date')); + $this->assertEquals($this->user->present()->name(), $this->getFirstValueByColumn($csv, 'Client User')); + $this->assertEquals('', $this->getFirstValueByColumn($csv, 'Client Assigned User')); + $this->assertEquals('USD', $this->getFirstValueByColumn($csv, 'Client Currency')); + + } + + + + + public function testClientContactCsvGeneration() { From 40a16226ebd148c1b80578e7362b78ca149a3f89 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Jul 2023 12:01:35 +1000 Subject: [PATCH 14/32] Fixes for proteted downloads --- app/Http/Controllers/ProtectedDownloadController.php | 6 ++++-- routes/api.php | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/ProtectedDownloadController.php b/app/Http/Controllers/ProtectedDownloadController.php index d7277d69f383..c333539b99ab 100644 --- a/app/Http/Controllers/ProtectedDownloadController.php +++ b/app/Http/Controllers/ProtectedDownloadController.php @@ -29,8 +29,10 @@ class ProtectedDownloadController extends BaseController throw new SystemError('File no longer available', 404); abort(404, 'File no longer available'); } - - return response()->download($hashed_path, basename($hashed_path), [])->deleteFileAfterSend(true); + + return response()->streamDownload(function () use ($hashed_path) { + echo Storage::get($hashed_path); + }, basename($hashed_path), []); } diff --git a/routes/api.php b/routes/api.php index 97c8550b3417..56de0b9dd319 100644 --- a/routes/api.php +++ b/routes/api.php @@ -406,6 +406,6 @@ Route::post('api/v1/yodlee/data_updates', [YodleeController::class, 'dataUpdates Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1'); Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1'); -Route::get('api/v1/protected_download/{hash}', [ProtectedDownloadController::class, 'index'])->name('protected_download')->middleware('signed')->middleware('throttle:300,1'); +Route::get('api/v1/protected_download/{hash}', [ProtectedDownloadController::class, 'index'])->name('protected_download')->middleware('throttle:300,1'); Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404'); \ No newline at end of file From 420fd1ef669fa66e3899d8317cee826c1bac0e85 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Jul 2023 18:34:26 +1000 Subject: [PATCH 15/32] Calculate this and last financial years --- app/Export/CSV/ActivityExport.php | 3 +- app/Export/CSV/BaseExport.php | 16 ++- app/Export/CSV/ClientExport.php | 2 - app/Export/CSV/ContactExport.php | 1 - app/Export/CSV/CreditExport.php | 1 - app/Export/CSV/DocumentExport.php | 1 - app/Export/CSV/ExpenseExport.php | 1 - app/Export/CSV/InvoiceExport.php | 2 - app/Export/CSV/InvoiceItemExport.php | 1 - app/Export/CSV/PaymentExport.php | 2 - app/Export/CSV/ProductExport.php | 2 - app/Export/CSV/ProductSalesExport.php | 2 - app/Export/CSV/PurchaseOrderExport.php | 1 - app/Export/CSV/PurchaseOrderItemExport.php | 1 - app/Export/CSV/QuoteExport.php | 1 - app/Export/CSV/QuoteItemExport.php | 1 - app/Export/CSV/RecurringInvoiceExport.php | 1 - app/Export/CSV/TaskExport.php | 1 - app/Export/CSV/VendorExport.php | 1 - app/Utils/Traits/MakesDates.php | 26 +++- tests/Unit/DatesTest.php | 144 +++++++++++++++++++++ 21 files changed, 182 insertions(+), 29 deletions(-) diff --git a/app/Export/CSV/ActivityExport.php b/app/Export/CSV/ActivityExport.php index 09d555fc1954..21aa0ecb0979 100644 --- a/app/Export/CSV/ActivityExport.php +++ b/app/Export/CSV/ActivityExport.php @@ -24,8 +24,7 @@ use App\Transformers\ActivityTransformer; class ActivityExport extends BaseExport { - private Company $company; - + private $entity_transformer; public string $date_key = 'created_at'; diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 1c299790d1cf..fe876bf989ad 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -13,14 +13,13 @@ namespace App\Export\CSV; use App\Utils\Number; use App\Models\Client; +use App\Models\Company; use App\Models\Expense; use App\Models\Invoice; -use App\Models\GatewayType; use App\Models\Payment; use League\Fractal\Manager; use Illuminate\Support\Carbon; use App\Utils\Traits\MakesHash; -use App\Transformers\ClientTransformer; use App\Transformers\PaymentTransformer; use Illuminate\Database\Eloquent\Builder; use League\Fractal\Serializer\ArraySerializer; @@ -29,6 +28,8 @@ class BaseExport { use MakesHash; + public Company $company; + public array $input; public string $date_key = ''; @@ -726,8 +727,15 @@ class BaseExport $this->end_date = now()->startOfDay()->format('Y-m-d'); return $query->whereBetween($this->date_key, [now()->subDays(365), now()])->orderBy($this->date_key, 'ASC'); case 'this_year': - $this->start_date = now()->startOfYear()->format('Y-m-d'); - $this->end_date = now()->format('Y-m-d'); + + $first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYearNoOverflow(); + + $this->start_date = $fin_year_start->format('Y-m-d'); + $this->end_date = $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d'); return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC'); case 'custom': $this->start_date = $custom_start_date->format('Y-m-d'); diff --git a/app/Export/CSV/ClientExport.php b/app/Export/CSV/ClientExport.php index 7d645c78b410..32f42ba807b7 100644 --- a/app/Export/CSV/ClientExport.php +++ b/app/Export/CSV/ClientExport.php @@ -22,8 +22,6 @@ use League\Csv\Writer; class ClientExport extends BaseExport { - private $company; - private $client_transformer; private $contact_transformer; diff --git a/app/Export/CSV/ContactExport.php b/app/Export/CSV/ContactExport.php index e960a572fa05..04ca04797d82 100644 --- a/app/Export/CSV/ContactExport.php +++ b/app/Export/CSV/ContactExport.php @@ -23,7 +23,6 @@ use League\Csv\Writer; class ContactExport extends BaseExport { - private Company $company; private ClientTransformer $client_transformer; diff --git a/app/Export/CSV/CreditExport.php b/app/Export/CSV/CreditExport.php index 67b259971003..1cadfea5b7dd 100644 --- a/app/Export/CSV/CreditExport.php +++ b/app/Export/CSV/CreditExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class CreditExport extends BaseExport { - private Company $company; private CreditTransformer $credit_transformer; diff --git a/app/Export/CSV/DocumentExport.php b/app/Export/CSV/DocumentExport.php index 3b7d06ede415..b43a759b5254 100644 --- a/app/Export/CSV/DocumentExport.php +++ b/app/Export/CSV/DocumentExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class DocumentExport extends BaseExport { - private Company $company; private $entity_transformer; diff --git a/app/Export/CSV/ExpenseExport.php b/app/Export/CSV/ExpenseExport.php index 9f542fea2316..70bb8c841edb 100644 --- a/app/Export/CSV/ExpenseExport.php +++ b/app/Export/CSV/ExpenseExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class ExpenseExport extends BaseExport { - private Company $company; private $expense_transformer; diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 98ed8498a6c3..853da158d23d 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -23,8 +23,6 @@ use App\Transformers\InvoiceTransformer; class InvoiceExport extends BaseExport { - private Company $company; - private $invoice_transformer; public string $date_key = 'date'; diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index 1f82c3407da8..943844bc125a 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -22,7 +22,6 @@ use League\Csv\Writer; class InvoiceItemExport extends BaseExport { - private Company $company; private $invoice_transformer; diff --git a/app/Export/CSV/PaymentExport.php b/app/Export/CSV/PaymentExport.php index e7aaf1abb9ed..dab8054c132f 100644 --- a/app/Export/CSV/PaymentExport.php +++ b/app/Export/CSV/PaymentExport.php @@ -21,8 +21,6 @@ use League\Csv\Writer; class PaymentExport extends BaseExport { - private Company $company; - private $entity_transformer; public string $date_key = 'date'; diff --git a/app/Export/CSV/ProductExport.php b/app/Export/CSV/ProductExport.php index de1b41e031fc..f1a24d49b48b 100644 --- a/app/Export/CSV/ProductExport.php +++ b/app/Export/CSV/ProductExport.php @@ -22,8 +22,6 @@ use League\Csv\Writer; class ProductExport extends BaseExport { - private Company $company; - private $entity_transformer; public string $date_key = 'created_at'; diff --git a/app/Export/CSV/ProductSalesExport.php b/app/Export/CSV/ProductSalesExport.php index a3228dfcdb2f..72017220d501 100644 --- a/app/Export/CSV/ProductSalesExport.php +++ b/app/Export/CSV/ProductSalesExport.php @@ -23,8 +23,6 @@ use League\Csv\Writer; class ProductSalesExport extends BaseExport { - private Company $company; - public string $date_key = 'created_at'; protected Collection $products; diff --git a/app/Export/CSV/PurchaseOrderExport.php b/app/Export/CSV/PurchaseOrderExport.php index 0b4202ce5af0..68ec10dc3219 100644 --- a/app/Export/CSV/PurchaseOrderExport.php +++ b/app/Export/CSV/PurchaseOrderExport.php @@ -22,7 +22,6 @@ use League\Csv\Writer; class PurchaseOrderExport extends BaseExport { - private Company $company; private $purchase_order_transformer; diff --git a/app/Export/CSV/PurchaseOrderItemExport.php b/app/Export/CSV/PurchaseOrderItemExport.php index 4847a162f9c0..0d83c93d388e 100644 --- a/app/Export/CSV/PurchaseOrderItemExport.php +++ b/app/Export/CSV/PurchaseOrderItemExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class PurchaseOrderItemExport extends BaseExport { - private Company $company; private $purchase_order_transformer; diff --git a/app/Export/CSV/QuoteExport.php b/app/Export/CSV/QuoteExport.php index 5711e46067b5..2f7d5d736c44 100644 --- a/app/Export/CSV/QuoteExport.php +++ b/app/Export/CSV/QuoteExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class QuoteExport extends BaseExport { - private Company $company; private $quote_transformer; diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index ffa5bb5a96de..7d310d93eaa0 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class QuoteItemExport extends BaseExport { - private Company $company; private $quote_transformer; diff --git a/app/Export/CSV/RecurringInvoiceExport.php b/app/Export/CSV/RecurringInvoiceExport.php index dfce445df17b..877fd04710fc 100644 --- a/app/Export/CSV/RecurringInvoiceExport.php +++ b/app/Export/CSV/RecurringInvoiceExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class RecurringInvoiceExport extends BaseExport { - private Company $company; private $invoice_transformer; diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 3f51da154851..fa60698e4792 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -24,7 +24,6 @@ use League\Csv\Writer; class TaskExport extends BaseExport { - private Company $company; private $entity_transformer; diff --git a/app/Export/CSV/VendorExport.php b/app/Export/CSV/VendorExport.php index e978c36e1432..dfc49f86daa5 100644 --- a/app/Export/CSV/VendorExport.php +++ b/app/Export/CSV/VendorExport.php @@ -22,7 +22,6 @@ use League\Csv\Writer; class VendorExport extends BaseExport { - private $company; private $vendor_transformer; diff --git a/app/Utils/Traits/MakesDates.php b/app/Utils/Traits/MakesDates.php index caea8e926d64..d08be2d47826 100644 --- a/app/Utils/Traits/MakesDates.php +++ b/app/Utils/Traits/MakesDates.php @@ -121,6 +121,30 @@ trait MakesDates */ public function calculateStartAndEndDates(array $data): array { + //override for financial years + if($data['date_range'] == 'this_year') { + $first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYearNoOverflow(); + + } + + //override for financial years + if($data['date_range'] == 'last_year') { + $first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) { + $fin_year_start->subYearNoOverflow(); + } + + } + + return match ($data['date_range']) { EmailStatement::LAST7 => [now()->startOfDay()->subDays(7)->format('Y-m-d'), now()->startOfDay()->format('Y-m-d')], EmailStatement::LAST30 => [now()->startOfDay()->subDays(30)->format('Y-m-d'), now()->startOfDay()->format('Y-m-d')], @@ -129,7 +153,7 @@ trait MakesDates EmailStatement::LAST_MONTH => [now()->startOfDay()->subMonthNoOverflow()->firstOfMonth()->format('Y-m-d'), now()->startOfDay()->subMonthNoOverflow()->lastOfMonth()->format('Y-m-d')], EmailStatement::THIS_QUARTER => [now()->startOfDay()->firstOfQuarter()->format('Y-m-d'), now()->startOfDay()->lastOfQuarter()->format('Y-m-d')], EmailStatement::LAST_QUARTER => [now()->startOfDay()->subQuarterNoOverflow()->firstOfQuarter()->format('Y-m-d'), now()->startOfDay()->subQuarterNoOverflow()->lastOfQuarter()->format('Y-m-d')], - EmailStatement::THIS_YEAR => [now()->startOfDay()->firstOfYear()->format('Y-m-d'), now()->startOfDay()->lastOfYear()->format('Y-m-d')], + EmailStatement::THIS_YEAR => [$fin_year_start->format('Y-m-d'), $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')], EmailStatement::LAST_YEAR => [now()->startOfDay()->subYearNoOverflow()->firstOfYear()->format('Y-m-d'), now()->startOfDay()->subYearNoOverflow()->lastOfYear()->format('Y-m-d')], EmailStatement::CUSTOM_RANGE => [$data['start_date'], $data['end_date']], default => [now()->startOfDay()->firstOfMonth()->format('Y-m-d'), now()->startOfDay()->lastOfMonth()->format('Y-m-d')], diff --git a/tests/Unit/DatesTest.php b/tests/Unit/DatesTest.php index 0fbaf15987d1..56cda7834226 100644 --- a/tests/Unit/DatesTest.php +++ b/tests/Unit/DatesTest.php @@ -31,6 +31,150 @@ class DatesTest extends TestCase // $this->makeTestData(); } + public function testLastFinancialYear3() + { + $this->travelTo(now()->createFromDate(2020, 6, 30)); + + //override for financial years + $first_month_of_year = 7; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) { + $fin_year_start->subYearNoOverflow(); + } + + $this->assertEquals('2018-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2019-06-30', $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testLastFinancialYear2() + { + $this->travelTo(now()->createFromDate(2020, 7, 1)); + + //override for financial years + $first_month_of_year = 7; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) { + $fin_year_start->subYearNoOverflow(); + } + + $this->assertEquals('2019-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2020-06-30', $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testLastFinancialYear() + { + $this->travelTo(now()->createFromDate(2020, 12, 1)); + + //override for financial years + $first_month_of_year = 7; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) { + $fin_year_start->subYearNoOverflow(); + } + + $this->assertEquals('2019-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2020-06-30', $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testFinancialYearDates4() + { + $this->travelTo(now()->createFromDate(2020, 12, 1)); + + $first_month_of_year = 7; + + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYear(); + + $fin_year_end = $fin_year_start->copy()->addYear()->subDay(); + + $this->assertEquals('2020-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2021-06-30', $fin_year_end->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testFinancialYearDates3() + { + $this->travelTo(now()->createFromDate(2021, 12, 1)); + + $first_month_of_year = 7; + + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYear(); + + $fin_year_end = $fin_year_start->copy()->addYear()->subDay(); + + $this->assertEquals('2021-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2022-06-30', $fin_year_end->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testFinancialYearDates2() + { + $this->travelTo(now()->createFromDate(2021, 8, 1)); + + $first_month_of_year = 7; + + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYear(); + + $fin_year_end = $fin_year_start->copy()->addYear()->subDay(); + + $this->assertEquals('2021-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2022-06-30', $fin_year_end->format('Y-m-d')); + + $this->travelBack(); + + } + + + public function testFinancialYearDates() + { + $this->travelTo(now()->createFromDate(2021, 1, 1)); + + $first_month_of_year = 7; + + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYear(); + + $fin_year_end = $fin_year_start->copy()->addYear()->subDay(); + + $this->assertEquals('2020-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2021-06-30', $fin_year_end->format('Y-m-d')); + + $this->travelBack(); + + } + public function testDaysDiff() { $string_date = '2021-06-01'; From fa487e7f921bf6b08947c26c09ca9aa96a175a5b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Jul 2023 18:43:56 +1000 Subject: [PATCH 16/32] Add this year / last year --- app/Export/CSV/BaseExport.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index fe876bf989ad..31583b000bf2 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -734,6 +734,19 @@ class BaseExport if(now()->lt($fin_year_start)) $fin_year_start->subYearNoOverflow(); + $this->start_date = $fin_year_start->format('Y-m-d'); + $this->end_date = $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d'); + return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC'); + case 'last_year': + + $first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) + $fin_year_start->subYearNoOverflow(); + $this->start_date = $fin_year_start->format('Y-m-d'); $this->end_date = $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d'); return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC'); From 514bbacc350fb6609f260d182dd8c5fe1c160aff Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 17 Jul 2023 23:10:37 +1000 Subject: [PATCH 17/32] Change year calculations to financial year spans --- app/Http/Requests/Chart/ShowChartRequest.php | 2 +- app/Utils/Traits/MakesDates.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Http/Requests/Chart/ShowChartRequest.php b/app/Http/Requests/Chart/ShowChartRequest.php index f7b4f63ba7e0..39517cd29db2 100644 --- a/app/Http/Requests/Chart/ShowChartRequest.php +++ b/app/Http/Requests/Chart/ShowChartRequest.php @@ -45,7 +45,7 @@ class ShowChartRequest extends Request $input = $this->all(); if(isset($input['date_range'])) { - $dates = $this->calculateStartAndEndDates($input); + $dates = $this->calculateStartAndEndDates($input, auth()->user()->company()); $input['start_date'] = $dates[0]; $input['end_date'] = $dates[1]; } diff --git a/app/Utils/Traits/MakesDates.php b/app/Utils/Traits/MakesDates.php index d08be2d47826..69ccb7a8b3c4 100644 --- a/app/Utils/Traits/MakesDates.php +++ b/app/Utils/Traits/MakesDates.php @@ -14,6 +14,7 @@ namespace App\Utils\Traits; use DateTime; use DateTimeZone; use Carbon\Carbon; +use App\Models\Company; use App\DataMapper\Schedule\EmailStatement; /** @@ -119,11 +120,11 @@ trait MakesDates * * @return array [$start_date, $end_date]; */ - public function calculateStartAndEndDates(array $data): array + public function calculateStartAndEndDates(array $data, ?Company $company = null): array { //override for financial years if($data['date_range'] == 'this_year') { - $first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1; + $first_month_of_year = $company ? $company?->first_month_of_year : 1; $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); if(now()->lt($fin_year_start)) @@ -133,7 +134,7 @@ trait MakesDates //override for financial years if($data['date_range'] == 'last_year') { - $first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1; + $first_month_of_year = $company ? $company?->first_month_of_year : 1; $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); $fin_year_start->subYearNoOverflow(); @@ -154,7 +155,7 @@ trait MakesDates EmailStatement::THIS_QUARTER => [now()->startOfDay()->firstOfQuarter()->format('Y-m-d'), now()->startOfDay()->lastOfQuarter()->format('Y-m-d')], EmailStatement::LAST_QUARTER => [now()->startOfDay()->subQuarterNoOverflow()->firstOfQuarter()->format('Y-m-d'), now()->startOfDay()->subQuarterNoOverflow()->lastOfQuarter()->format('Y-m-d')], EmailStatement::THIS_YEAR => [$fin_year_start->format('Y-m-d'), $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')], - EmailStatement::LAST_YEAR => [now()->startOfDay()->subYearNoOverflow()->firstOfYear()->format('Y-m-d'), now()->startOfDay()->subYearNoOverflow()->lastOfYear()->format('Y-m-d')], + EmailStatement::LAST_YEAR => [$fin_year_start->format('Y-m-d'), $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')], EmailStatement::CUSTOM_RANGE => [$data['start_date'], $data['end_date']], default => [now()->startOfDay()->firstOfMonth()->format('Y-m-d'), now()->startOfDay()->lastOfMonth()->format('Y-m-d')], }; From 40abf68bc9d9d3847fca544dc498d8e2986bc2cd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 11:42:36 +1000 Subject: [PATCH 18/32] Log vendor imports --- app/Export/CSV/BaseExport.php | 1 - app/PaymentDrivers/CheckoutComPaymentDriver.php | 3 +++ app/Repositories/VendorRepository.php | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 31583b000bf2..c99a66475a6f 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -741,7 +741,6 @@ class BaseExport $first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1; $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); - $fin_year_start->subYearNoOverflow(); if(now()->subYear()->lt($fin_year_start)) diff --git a/app/PaymentDrivers/CheckoutComPaymentDriver.php b/app/PaymentDrivers/CheckoutComPaymentDriver.php index e54d76dd6881..77b258661c39 100644 --- a/app/PaymentDrivers/CheckoutComPaymentDriver.php +++ b/app/PaymentDrivers/CheckoutComPaymentDriver.php @@ -440,6 +440,9 @@ class CheckoutComPaymentDriver extends BaseDriver $request->query('cko-session-id') ); + nlog("checkout3ds"); + nlog($payment); + if (isset($payment['approved']) && $payment['approved']) { return $this->processSuccessfulPayment($payment); } else { diff --git a/app/Repositories/VendorRepository.php b/app/Repositories/VendorRepository.php index 4c4fc5fa6b23..bb9266014701 100644 --- a/app/Repositories/VendorRepository.php +++ b/app/Repositories/VendorRepository.php @@ -44,7 +44,9 @@ class VendorRepository extends BaseRepository public function save(array $data, Vendor $vendor) : ?Vendor { $vendor->fill($data); - + + nlog($data); + $vendor->saveQuietly(); if ($vendor->number == '' || ! $vendor->number) { From 54285d5d33b379a871eb387d0bca6fd90eef3156 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 13:15:40 +1000 Subject: [PATCH 19/32] per entity activities --- app/Helpers/Invoice/InvoiceItemSum.php | 12 +- app/Http/Controllers/ActivityController.php | 142 ++++++------------ .../Requests/Activity/ShowActivityRequest.php | 26 +++- .../Transformer/Csv/PaymentTransformer.php | 2 +- app/Repositories/BaseRepository.php | 2 + routes/api.php | 1 + tests/Feature/ActivityApiTest.php | 39 ++++- 7 files changed, 115 insertions(+), 109 deletions(-) diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index fe5c075b99ac..3d419a66ae9d 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -238,14 +238,20 @@ class InvoiceItemSum { $this->rule->tax($this->item); + $precision = strlen(substr(strrchr($this->rule->tax_rate1, "."), 1)); + $this->item->tax_name1 = $this->rule->tax_name1; - $this->item->tax_rate1 = $this->rule->tax_rate1; + $this->item->tax_rate1 = round($this->rule->tax_rate1, $precision); + + $precision = strlen(substr(strrchr($this->rule->tax_rate2, "."), 1)); $this->item->tax_name2 = $this->rule->tax_name2; - $this->item->tax_rate2 = $this->rule->tax_rate2; + $this->item->tax_rate2 = round($this->rule->tax_rate2, $precision); + + $precision = strlen(substr(strrchr($this->rule->tax_rate3, "."), 1)); $this->item->tax_name3 = $this->rule->tax_name3; - $this->item->tax_rate3 = $this->rule->tax_rate3; + $this->item->tax_rate3 = round($this->rule->tax_rate3, $precision); return $this; } diff --git a/app/Http/Controllers/ActivityController.php b/app/Http/Controllers/ActivityController.php index cf69f531cbfd..b201296e0db4 100644 --- a/app/Http/Controllers/ActivityController.php +++ b/app/Http/Controllers/ActivityController.php @@ -11,24 +11,23 @@ namespace App\Http\Controllers; -use App\Http\Requests\Activity\DownloadHistoricalEntityRequest; -use App\Models\Activity; -use App\Transformers\ActivityTransformer; -use App\Utils\HostedPDF\NinjaPdf; -use App\Utils\Ninja; -use App\Utils\PhantomJS\Phantom; -use App\Utils\Traits\Pdf\PageNumbering; -use App\Utils\Traits\Pdf\PdfMaker; -use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; -use Illuminate\Http\Response; -use Illuminate\Support\Facades\Storage; use stdClass; -use Symfony\Component\HttpFoundation\StreamedResponse; +use App\Utils\Ninja; +use App\Models\Activity; +use Illuminate\Http\Request; +use App\Utils\Traits\MakesHash; +use App\Utils\PhantomJS\Phantom; +use App\Utils\HostedPDF\NinjaPdf; +use App\Utils\Traits\Pdf\PdfMaker; +use App\Utils\Traits\Pdf\PageNumbering; +use Illuminate\Support\Facades\Storage; +use App\Transformers\ActivityTransformer; +use App\Http\Requests\Activity\ShowActivityRequest; +use App\Http\Requests\Activity\DownloadHistoricalEntityRequest; class ActivityController extends BaseController { - use PdfMaker, PageNumbering; + use PdfMaker, PageNumbering, MakesHash; protected $entity_type = Activity::class; @@ -39,50 +38,6 @@ class ActivityController extends BaseController parent::__construct(); } - /** - * @OA\Get( - * path="/api/v1/activities", - * operationId="getActivities", - * tags={"actvities"}, - * summary="Gets a list of actvities", - * description="Lists all activities", - * @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"), - * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), - * @OA\Parameter(ref="#/components/parameters/include"), - * @OA\Parameter(ref="#/components/parameters/index"), - * @OA\Parameter( - * name="rows", - * in="query", - * description="The number of activities to return", - * example="50", - * required=false, - * @OA\Schema( - * type="number", - * format="integer", - * ), - * ), - * @OA\Response( - * response=200, - * description="A list of actvities", - * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), - * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), - * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), - * @OA\JsonContent(ref="#/components/schemas/Activity"), - * ), - * @OA\Response( - * response=422, - * description="Validation error", - * @OA\JsonContent(ref="#/components/schemas/ValidationError"), - * ), - * @OA\Response( - * response="default", - * description="Unexpected Error", - * @OA\JsonContent(ref="#/components/schemas/Error"), - * ), - * ) - * @param Request $request - * @return Response|mixed - */ public function index(Request $request) { $default_activities = $request->has('rows') ? $request->input('rows') : 75; @@ -115,47 +70,36 @@ class ActivityController extends BaseController return $this->listResponse($activities); } - /** - * @OA\Get( - * path="/api/v1/actvities/download_entity/{activity_id}", - * operationId="getActivityHistoricalEntityPdf", - * tags={"actvities"}, - * summary="Gets a PDF for the given activity", - * description="Gets a PDF for the given activity", - * @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"), - * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), - * @OA\Parameter( - * name="activity_id", - * in="path", - * description="The Activity Hashed ID", - * example="D2J234DFA", - * required=true, - * @OA\Schema( - * type="string", - * format="string", - * ), - * ), - * @OA\Response( - * response=200, - * description="PDF File", - * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), - * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), - * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), - * ), - * @OA\Response( - * response=404, - * description="No file exists for the given record", - * ), - * @OA\Response( - * response="default", - * description="Unexpected Error", - * @OA\JsonContent(ref="#/components/schemas/Error"), - * ), - * ) - * @param DownloadHistoricalEntityRequest $request - * @param Activity $activity - * @return JsonResponse|StreamedResponse - */ + public function entityActivity(ShowActivityRequest $request) + { + + $default_activities = request()->has('rows') ? request()->input('rows') : 75; + + $activities = Activity::with('user') + ->orderBy('created_at', 'DESC') + ->company() + ->where("{$request->entity}_id", $request->entity_id) + ->take($default_activities); + + /** @var \App\Models\User auth()->user() */ + $user = auth()->user(); + + if (!$user->isAdmin()) { + $activities->where('user_id', auth()->user()->id); + } + + $system = ctrans('texts.system'); + + $data = $activities->cursor()->map(function ($activity) use ($system) { + + return $activity->activity_string(); + + }); + + return response()->json(['data' => $data->toArray()], 200); + + } + public function downloadHistoricalEntity(DownloadHistoricalEntityRequest $request, Activity $activity) { $backup = $activity->backup; diff --git a/app/Http/Requests/Activity/ShowActivityRequest.php b/app/Http/Requests/Activity/ShowActivityRequest.php index d7c745cb5d10..fd4b50a9bb13 100644 --- a/app/Http/Requests/Activity/ShowActivityRequest.php +++ b/app/Http/Requests/Activity/ShowActivityRequest.php @@ -12,10 +12,12 @@ namespace App\Http\Requests\Activity; use App\Http\Requests\Request; -use App\Models\Activity; +use App\Utils\Traits\MakesHash; class ShowActivityRequest extends Request { + use MakesHash; + /** * Determine if the user is authorized to make this request. * @@ -23,7 +25,25 @@ class ShowActivityRequest extends Request */ public function authorize() : bool { - // return auth()->user()->isAdmin(); - return auth()->user()->can('view', Activity::class); + return true; + } + + public function rules() + { + return [ + 'entity' => 'bail|required|in:invoice,quote,credit,purchase_order,payment,client,vendor', + 'entity_id' => 'bail|required|exists:'.$this->entity.'s,id,company_id,'.auth()->user()->company()->id, + ]; + } + + public function prepareForValidation() + { + $input = $this->all(); + + if(isset($input['entity_id'])) + $input['entity_id'] = $this->decodePrimaryKey($input['entity_id']); + + $this->replace($input); + } } diff --git a/app/Import/Transformer/Csv/PaymentTransformer.php b/app/Import/Transformer/Csv/PaymentTransformer.php index 3ea61e845d47..44fcc9de52e4 100644 --- a/app/Import/Transformer/Csv/PaymentTransformer.php +++ b/app/Import/Transformer/Csv/PaymentTransformer.php @@ -46,7 +46,7 @@ class PaymentTransformer extends BaseTransformer $data, 'payment.transaction_reference ' ), - 'date' => $this->getString($data, 'payment.date'), + 'date' => isset($data['payment.date']) ? $this->parseDate($data['payment.date']) : date('y-m-d'), 'private_notes' => $this->getString($data, 'payment.private_notes'), 'custom_value1' => $this->getString($data, 'payment.custom_value1'), 'custom_value2' => $this->getString($data, 'payment.custom_value2'), diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index 15c93b5fe556..0350cb00b95a 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -180,6 +180,8 @@ class BaseRepository unset($tmp_data['client_contacts']); } + nlog($tmp_data); + $model->fill($tmp_data); $model->custom_surcharge_tax1 = $client->company->custom_surcharge_taxes1; diff --git a/routes/api.php b/routes/api.php index 56de0b9dd319..0331edea846f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -143,6 +143,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::get('health_check', [PingController::class, 'health'])->name('health_check'); Route::get('activities', [ActivityController::class, 'index']); + Route::post('activities/entity', [ActivityController::class, 'entityActivity']); Route::get('activities/download_entity/{activity}', [ActivityController::class, 'downloadHistoricalEntity']); Route::post('charts/totals', [ChartController::class, 'totals'])->name('chart.totals'); diff --git a/tests/Feature/ActivityApiTest.php b/tests/Feature/ActivityApiTest.php index 271b54d2ad0c..60aeb5e5a503 100644 --- a/tests/Feature/ActivityApiTest.php +++ b/tests/Feature/ActivityApiTest.php @@ -11,10 +11,11 @@ namespace Tests\Feature; -use Illuminate\Foundation\Testing\DatabaseTransactions; -use Illuminate\Routing\Middleware\ThrottleRequests; -use Tests\MockAccountData; use Tests\TestCase; +use Tests\MockAccountData; +use Illuminate\Validation\ValidationException; +use Illuminate\Routing\Middleware\ThrottleRequests; +use Illuminate\Foundation\Testing\DatabaseTransactions; /** * @test @@ -34,6 +35,38 @@ class ActivityApiTest extends TestCase $this->withoutMiddleware( ThrottleRequests::class ); + + $this->withoutExceptionHandling(); + + } + + public function testActivityEntity() + { + + $invoice = $this->company->invoices()->first(); + + $invoice->service()->markSent()->markPaid()->markDeleted()->handleRestore()->save(); + + $data = [ + 'entity' => 'invoice', + 'entity_id' => $invoice->hashed_id + ]; + + $response = false; + + try { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/activities/entity', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + } + + $response->assertStatus(200); + + } public function testActivityGet() From 021ea34bc1ba42c0932005a7e304076c84dd296c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 13:17:19 +1000 Subject: [PATCH 20/32] Per entity activities --- app/Http/Requests/Activity/ShowActivityRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Requests/Activity/ShowActivityRequest.php b/app/Http/Requests/Activity/ShowActivityRequest.php index fd4b50a9bb13..219844f35dbe 100644 --- a/app/Http/Requests/Activity/ShowActivityRequest.php +++ b/app/Http/Requests/Activity/ShowActivityRequest.php @@ -31,7 +31,7 @@ class ShowActivityRequest extends Request public function rules() { return [ - 'entity' => 'bail|required|in:invoice,quote,credit,purchase_order,payment,client,vendor', + 'entity' => 'bail|required|in:invoice,quote,credit,purchase_order,payment,client,vendor,expense,task,project,subscription,recurring_invoice,', 'entity_id' => 'bail|required|exists:'.$this->entity.'s,id,company_id,'.auth()->user()->company()->id, ]; } From eba7105fdd290c806e342d852de51b1bb2d2f566 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 13:43:33 +1000 Subject: [PATCH 21/32] Tests for output --- .../Export/ReportCsvGenerationTest.php | 116 +++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 3f074cf922f1..5eceb3cab6d2 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -335,9 +335,123 @@ class ReportCsvGenerationTest extends TestCase } + public function testCreditCustomColumnsCsvGeneration() + { + + Credit::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","credit.number","credit.amount","payment.date", "payment.amount"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/credits', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Credit Credit Number')); + $this->assertEquals('Unpaid', $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals('', $this->getFirstValueByColumn($csv, 'Payment Date')); + + } + + public function testInvoiceCustomColumnsCsvGeneration() + { + + \App\Models\Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","invoice.number","invoice.amount","payment.date", "payment.amount"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/invoices', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals('Unpaid', $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals('', $this->getFirstValueByColumn($csv, 'Payment Date')); + + } + + public function testInvoicePaidCustomColumnsCsvGeneration() + { + + $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->service()->markPaid()->save(); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","invoice.number","invoice.amount","payment.date", "payment.amount"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/invoices', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('12345', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals('$100.00', $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals(now()->addSeconds($this->company->timezone()->utc_offset)->format('Y-m-d'), $this->getFirstValueByColumn($csv, 'Payment Date')); - + } public function testClientContactCsvGeneration() { From 0a5747998d404a0e1182e958aea964059610a310 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 14:00:06 +1000 Subject: [PATCH 22/32] v5.6.19 --- .../Export/ReportCsvGenerationTest.php | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 5eceb3cab6d2..94c529dc68dd 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -410,6 +410,175 @@ class ReportCsvGenerationTest extends TestCase $this->assertEquals('', $this->getFirstValueByColumn($csv, 'Payment Date')); } + + public function testInvoiceItemsCustomColumnsCsvGeneration() + { + + \App\Models\Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'line_items' => [ + [ + 'quantity' => 10, + 'cost' => 100, + 'line_total' => 1000, + 'is_amount_discount' => true, + 'discount' => 0, + 'notes' => 'item notes', + 'product_key' => 'product key', + 'custom_value1' => 'custom 1', + 'custom_value2' => 'custom 2', + 'custom_value3' => 'custom 3', + 'custom_value4' => 'custom 4', + 'tax_name1' => 'GST', + 'tax_rate1' => 10.00, + 'type_id' => '1', + ], + ] + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","invoice.number","invoice.amount","payment.date", "payment.amount", "item.quantity", "item.cost", "item.line_total", "item.discount", "item.notes", "item.product_key", "item.custom_value1", "item.tax_name1", "item.tax_rate1",], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/invoice_items', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals('Unpaid', $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals('', $this->getFirstValueByColumn($csv, 'Payment Date')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Quantity')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Cost')); + $this->assertEquals('1000', $this->getFirstValueByColumn($csv, 'Line Total')); + $this->assertEquals('0', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('item notes', $this->getFirstValueByColumn($csv, 'Notes')); + $this->assertEquals('product key', $this->getFirstValueByColumn($csv, 'Product')); + $this->assertEquals('custom 1', $this->getFirstValueByColumn($csv, 'Custom Invoice 1')); + $this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + + } + + + public function testQuoteItemsCustomColumnsCsvGeneration() + { + + \App\Models\Quote::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'line_items' => [ + [ + 'quantity' => 10, + 'cost' => 100, + 'line_total' => 1000, + 'is_amount_discount' => true, + 'discount' => 0, + 'notes' => 'item notes', + 'product_key' => 'product key', + 'custom_value1' => 'custom 1', + 'custom_value2' => 'custom 2', + 'custom_value3' => 'custom 3', + 'custom_value4' => 'custom 4', + 'tax_name1' => 'GST', + 'tax_rate1' => 10.00, + 'type_id' => '1', + ], + ] + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","quote.number","quote.amount", "item.quantity", "item.cost", "item.line_total", "item.discount", "item.notes", "item.product_key", "item.custom_value1", "item.tax_name1", "item.tax_rate1",], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/quote_items', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Quote Number')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Quantity')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Cost')); + $this->assertEquals('1000', $this->getFirstValueByColumn($csv, 'Line Total')); + $this->assertEquals('0', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('item notes', $this->getFirstValueByColumn($csv, 'Notes')); + $this->assertEquals('product key', $this->getFirstValueByColumn($csv, 'Product')); + $this->assertEquals('custom 1', $this->getFirstValueByColumn($csv, 'Custom Quote 1')); + $this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + + } + + + + public function testQuoteCustomColumnsCsvGeneration() + { + + \App\Models\Quote::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","quote.number","quote.amount"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/quotes', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Quote Number')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Quote Amount')); + + } + public function testInvoicePaidCustomColumnsCsvGeneration() { @@ -801,5 +970,50 @@ class ReportCsvGenerationTest extends TestCase } + public function testExpenseCustomColumnsCsvGeneration() + { + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + ] + + ); + + Expense::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'vendor_id' => $vendor->id, + 'amount' => 100, + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'currency_id' => 1, + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ['client.name','vendor.name','expense.amount','expense.currency_id'], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/expenses', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Vendor Name')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('USD', $this->getFirstValueByColumn($csv, 'Currency')); + + } + } \ No newline at end of file From 6af38604affc5fe56c5317054200fb6eccd2b20e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 14:26:52 +1000 Subject: [PATCH 23/32] Fixes for custom values --- app/Export/CSV/BaseExport.php | 12 ++++++++++-- app/Export/CSV/QuoteItemExport.php | 7 ++++--- tests/Feature/Export/ReportCsvGenerationTest.php | 3 ++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index c99a66475a6f..e1bb510896cb 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -764,7 +764,7 @@ class BaseExport { $header = []; - // nlog($this->input['report_keys']); + nlog($this->input['report_keys']); foreach (array_merge($this->input['report_keys'], $this->forced_keys) as $value) { @@ -839,7 +839,15 @@ class BaseExport $key = str_replace('payment.', '', $key); $key = str_replace('expense.', '', $key); - $header[] = "{$prefix}" . ctrans("texts.{$key}"); + if(in_array($key, ['quote1','quote2','quote3','quote4','credit1','credit2','credit3','credit4','purchase_order1','purchase_order2','purchase_order3','purchase_order4'])) + { + $number = substr($key, -1); + $header[] = ctrans('texts.item') . " ". ctrans("texts.custom_value{$number}"); + } + else + { + $header[] = "{$prefix}" . ctrans("texts.{$key}"); + } } // nlog($header); diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index 7d310d93eaa0..9af20df7b5ca 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -62,10 +62,11 @@ class QuoteItemExport extends BaseExport 'terms' => 'terms', 'total_taxes' => 'total_taxes', 'currency' => 'currency_id', - 'qty' => 'item.quantity', - 'unit_cost' => 'item.cost', + 'quantity' => 'item.quantity', + 'cost' => 'item.cost', 'product_key' => 'item.product_key', - 'cost' => 'item.product_cost', + 'buy_price' => 'item.product_cost', + 'cost' => 'item.cost', 'notes' => 'item.notes', 'discount' => 'item.discount', 'is_amount_discount' => 'item.is_amount_discount', diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 94c529dc68dd..946918126873 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -525,6 +525,7 @@ class ReportCsvGenerationTest extends TestCase ])->post('/api/v1/reports/quote_items', $data); $csv = $response->streamedContent(); +nlog($csv); $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Quote Number')); @@ -534,7 +535,7 @@ class ReportCsvGenerationTest extends TestCase $this->assertEquals('0', $this->getFirstValueByColumn($csv, 'Discount')); $this->assertEquals('item notes', $this->getFirstValueByColumn($csv, 'Notes')); $this->assertEquals('product key', $this->getFirstValueByColumn($csv, 'Product')); - $this->assertEquals('custom 1', $this->getFirstValueByColumn($csv, 'Custom Quote 1')); + $this->assertEquals('custom 1', $this->getFirstValueByColumn($csv, 'Item Custom Value 1')); $this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Tax Name 1')); $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); From 45370ff5c41cb237bdfe9af1f126e634b5ca8987 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 14:39:04 +1000 Subject: [PATCH 24/32] Fixes for purchase order export --- app/Export/CSV/BaseExport.php | 1 + app/Export/CSV/PurchaseOrderExport.php | 68 +++++----- app/Export/CSV/PurchaseOrderItemExport.php | 34 ++--- .../Export/ReportCsvGenerationTest.php | 128 +++++++++++++++++- 4 files changed, 179 insertions(+), 52 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index e1bb510896cb..e29ce936c0ad 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -829,6 +829,7 @@ class BaseExport $key = str_replace('item.', '', $key); $key = str_replace('recurring_invoice.', '', $key); + $key = str_replace('purchase_order.', '', $key); $key = str_replace('invoice.', '', $key); $key = str_replace('quote.', '', $key); $key = str_replace('credit.', '', $key); diff --git a/app/Export/CSV/PurchaseOrderExport.php b/app/Export/CSV/PurchaseOrderExport.php index 68ec10dc3219..bfba71830f3c 100644 --- a/app/Export/CSV/PurchaseOrderExport.php +++ b/app/Export/CSV/PurchaseOrderExport.php @@ -30,39 +30,39 @@ class PurchaseOrderExport extends BaseExport public Writer $csv; public array $entity_keys = [ - 'amount' => 'amount', - 'balance' => 'balance', - 'vendor' => 'vendor_id', - 'custom_surcharge1' => 'custom_surcharge1', - 'custom_surcharge2' => 'custom_surcharge2', - 'custom_surcharge3' => 'custom_surcharge3', - 'custom_surcharge4' => 'custom_surcharge4', - 'custom_value1' => 'custom_value1', - 'custom_value2' => 'custom_value2', - 'custom_value3' => 'custom_value3', - 'custom_value4' => 'custom_value4', - 'date' => 'date', - 'discount' => 'discount', - 'due_date' => 'due_date', - 'exchange_rate' => 'exchange_rate', - 'footer' => 'footer', - 'number' => 'number', - 'paid_to_date' => 'paid_to_date', - 'partial' => 'partial', - 'partial_due_date' => 'partial_due_date', - 'po_number' => 'po_number', - 'private_notes' => 'private_notes', - 'public_notes' => 'public_notes', - 'status' => 'status_id', - 'tax_name1' => 'tax_name1', - 'tax_name2' => 'tax_name2', - 'tax_name3' => 'tax_name3', - 'tax_rate1' => 'tax_rate1', - 'tax_rate2' => 'tax_rate2', - 'tax_rate3' => 'tax_rate3', - 'terms' => 'terms', - 'total_taxes' => 'total_taxes', - 'currency_id' => 'currency_id', + 'amount' => 'purchase_order.amount', + 'balance' => 'purchase_order.balance', + 'vendor' => 'purchase_order.vendor_id', + // 'custom_surcharge1' => 'purchase_order.custom_surcharge1', + // 'custom_surcharge2' => 'purchase_order.custom_surcharge2', + // 'custom_surcharge3' => 'purchase_order.custom_surcharge3', + // 'custom_surcharge4' => 'purchase_order.custom_surcharge4', + 'custom_value1' => 'purchase_order.custom_value1', + 'custom_value2' => 'purchase_order.custom_value2', + 'custom_value3' => 'purchase_order.custom_value3', + 'custom_value4' => 'purchase_order.custom_value4', + 'date' => 'purchase_order.date', + 'discount' => 'purchase_order.discount', + 'due_date' => 'purchase_order.due_date', + 'exchange_rate' => 'purchase_order.exchange_rate', + 'footer' => 'purchase_order.footer', + 'number' => 'purchase_order.number', + 'paid_to_date' => 'purchase_order.paid_to_date', + 'partial' => 'purchase_order.partial', + 'partial_due_date' => 'purchase_order.partial_due_date', + 'po_number' => 'purchase_order.po_number', + 'private_notes' => 'purchase_order.private_notes', + 'public_notes' => 'purchase_order.public_notes', + 'status' => 'purchase_order.status_id', + 'tax_name1' => 'purchase_order.tax_name1', + 'tax_name2' => 'purchase_order.tax_name2', + 'tax_name3' => 'purchase_order.tax_name3', + 'tax_rate1' => 'purchase_order.tax_rate1', + 'tax_rate2' => 'purchase_order.tax_rate2', + 'tax_rate3' => 'purchase_order.tax_rate3', + 'terms' => 'purchase_order.terms', + 'total_taxes' => 'purchase_order.total_taxes', + 'currency_id' => 'purchase_order.currency_id', ]; private array $decorate_keys = [ @@ -129,7 +129,7 @@ class PurchaseOrderExport extends BaseExport $keyval = array_search($key, $this->entity_keys); if(!$keyval) { - $keyval = array_search(str_replace("invoice.", "", $key), $this->entity_keys) ?? $key; + $keyval = array_search(str_replace("purchase_order.", "", $key), $this->entity_keys) ?? $key; } if(!$keyval) { diff --git a/app/Export/CSV/PurchaseOrderItemExport.php b/app/Export/CSV/PurchaseOrderItemExport.php index 0d83c93d388e..2bb1eef01591 100644 --- a/app/Export/CSV/PurchaseOrderItemExport.php +++ b/app/Export/CSV/PurchaseOrderItemExport.php @@ -36,10 +36,10 @@ class PurchaseOrderItemExport extends BaseExport 'vendor' => 'vendor_id', 'vendor_number' => 'vendor.number', 'vendor_id_number' => 'vendor.id_number', - 'custom_surcharge1' => 'custom_surcharge1', - 'custom_surcharge2' => 'custom_surcharge2', - 'custom_surcharge3' => 'custom_surcharge3', - 'custom_surcharge4' => 'custom_surcharge4', + // 'custom_surcharge1' => 'custom_surcharge1', + // 'custom_surcharge2' => 'custom_surcharge2', + // 'custom_surcharge3' => 'custom_surcharge3', + // 'custom_surcharge4' => 'custom_surcharge4', // 'custom_value1' => 'custom_value1', // 'custom_value2' => 'custom_value2', // 'custom_value3' => 'custom_value3', @@ -81,10 +81,10 @@ class PurchaseOrderItemExport extends BaseExport 'tax_name3' => 'item.tax_name3', 'line_total' => 'item.line_total', 'gross_line_total' => 'item.gross_line_total', - // 'invoice1' => 'item.custom_value1', - // 'invoice2' => 'item.custom_value2', - // 'invoice3' => 'item.custom_value3', - // 'invoice4' => 'item.custom_value4', + 'purchase_order1' => 'item.custom_value1', + 'purchase_order2' => 'item.custom_value2', + 'purchase_order3' => 'item.custom_value3', + 'purchase_order4' => 'item.custom_value4', 'tax_category' => 'item.tax_id', 'type' => 'item.type_id', ]; @@ -138,7 +138,7 @@ class PurchaseOrderItemExport extends BaseExport private function iterateItems(PurchaseOrder $purchase_order) { - $transformed_invoice = $this->buildRow($purchase_order); + $transformed_purchase_order = $this->buildRow($purchase_order); $transformed_items = []; @@ -153,7 +153,7 @@ class PurchaseOrderItemExport extends BaseExport $keyval = $key; - $keyval = str_replace("custom_value", "invoice", $key); + $keyval = str_replace("custom_value", "purchase_order", $key); if($key == 'type_id') { $keyval = 'type'; @@ -183,7 +183,7 @@ class PurchaseOrderItemExport extends BaseExport } } - $transformed_items = array_merge($transformed_invoice, $item_array); + $transformed_items = array_merge($transformed_purchase_order, $item_array); $entity = $this->decorateAdvancedFields($purchase_order, $transformed_items); $this->csv->insertOne($entity); @@ -192,7 +192,7 @@ class PurchaseOrderItemExport extends BaseExport private function buildRow(PurchaseOrder $purchase_order) :array { - $transformed_invoice = $this->purchase_order_transformer->transform($purchase_order); + $transformed_purchase_order = $this->purchase_order_transformer->transform($purchase_order); $entity = []; @@ -200,17 +200,17 @@ class PurchaseOrderItemExport extends BaseExport $keyval = array_search($key, $this->entity_keys); if(!$keyval) { - $keyval = array_search(str_replace("invoice.", "", $key), $this->entity_keys) ?? $key; + $keyval = array_search(str_replace("purchase_order.", "", $key), $this->entity_keys) ?? $key; } if(!$keyval) { $keyval = $key; } - if (array_key_exists($key, $transformed_invoice)) { - $entity[$keyval] = $transformed_invoice[$key]; - } elseif (array_key_exists($keyval, $transformed_invoice)) { - $entity[$keyval] = $transformed_invoice[$keyval]; + if (array_key_exists($key, $transformed_purchase_order)) { + $entity[$keyval] = $transformed_purchase_order[$key]; + } elseif (array_key_exists($keyval, $transformed_purchase_order)) { + $entity[$keyval] = $transformed_purchase_order[$keyval]; } else { $entity[$keyval] = $this->resolveKey($keyval, $purchase_order, $this->purchase_order_transformer); } diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 946918126873..14abc9ff5a73 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -525,7 +525,7 @@ class ReportCsvGenerationTest extends TestCase ])->post('/api/v1/reports/quote_items', $data); $csv = $response->streamedContent(); -nlog($csv); + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Quote Number')); @@ -542,6 +542,132 @@ nlog($csv); } + public function testPurchaseOrderCsvGeneration() + { + + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + ] + ); + + \App\Models\PurchaseOrder::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'vendor_id' => $vendor->id, + 'amount' => 100, + 'balance' => 50, + 'status_id' => 2, + 'discount' => 10, + 'number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/purchase_orders', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); + } + + + public function testPurchaseOrderItemsCustomColumnsCsvGeneration() + { + + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + ] + ); + + + \App\Models\PurchaseOrder::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'vendor_id' => $vendor->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'po_number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'line_items' => [ + [ + 'quantity' => 10, + 'cost' => 100, + 'line_total' => 1000, + 'is_amount_discount' => true, + 'discount' => 0, + 'notes' => 'item notes', + 'product_key' => 'product key', + 'custom_value1' => 'custom 1', + 'custom_value2' => 'custom 2', + 'custom_value3' => 'custom 3', + 'custom_value4' => 'custom 4', + 'tax_name1' => 'GST', + 'tax_rate1' => 10.00, + 'type_id' => '1', + ], + ] + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["vendor.name","purchase_order.number","purchase_order.amount", "item.quantity", "item.cost", "item.line_total", "item.discount", "item.notes", "item.product_key", "item.custom_value1", "item.tax_name1", "item.tax_rate1",], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/purchase_order_items', $data); + + $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')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Cost')); + $this->assertEquals('1000', $this->getFirstValueByColumn($csv, 'Line Total')); + $this->assertEquals('0', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('item notes', $this->getFirstValueByColumn($csv, 'Notes')); + $this->assertEquals('product key', $this->getFirstValueByColumn($csv, 'Product')); + $this->assertEquals('custom 1', $this->getFirstValueByColumn($csv, 'Item Custom Value 1')); + $this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + + } public function testQuoteCustomColumnsCsvGeneration() { From 0cb4eba355336ee0835d9fd20ce1be9dc0ae65d9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 15:11:45 +1000 Subject: [PATCH 25/32] Recurring invoice exports --- app/Export/CSV/BaseExport.php | 39 +++++++++++++++-- app/Export/CSV/RecurringInvoiceExport.php | 28 +++++++++--- .../Export/ReportCsvGenerationTest.php | 43 ++++++++++++++++++- 3 files changed, 99 insertions(+), 11 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index e29ce936c0ad..ab2873a965a7 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -137,6 +137,35 @@ class BaseExport "user" => "invoice.user_id", ]; + protected array $recurring_invoice_report_keys = [ + "invoice_number" => "recurring_invoice.number", + "amount" => "recurring_invoice.amount", + "balance" => "recurring_invoice.balance", + "paid_to_date" => "recurring_invoice.paid_to_date", + "po_number" => "recurring_invoice.po_number", + "date" => "recurring_invoice.date", + "due_date" => "recurring_invoice.due_date", + "terms" => "recurring_invoice.terms", + "footer" => "recurring_invoice.footer", + "status" => "recurring_invoice.status", + "public_notes" => "recurring_invoice.public_notes", + "private_notes" => "recurring_invoice.private_notes", + "uses_inclusive_taxes" => "recurring_invoice.uses_inclusive_taxes", + "is_amount_discount" => "recurring_invoice.is_amount_discount", + "partial" => "recurring_invoice.partial", + "partial_due_date" => "recurring_invoice.partial_due_date", + "surcharge1" => "recurring_invoice.custom_surcharge1", + "surcharge2" => "recurring_invoice.custom_surcharge2", + "surcharge3" => "recurring_invoice.custom_surcharge3", + "surcharge4" => "recurring_invoice.custom_surcharge4", + "exchange_rate" => "recurring_invoice.exchange_rate", + "tax_amount" => "recurring_invoice.total_taxes", + "assigned_user" => "recurring_invoice.assigned_user_id", + "user" => "recurring_invoice.user_id", + "frequency_id" => "recurring_invoice.frequency_id", + "next_send_date" => "recurring_invoice.next_send_date" + ]; + protected array $purchase_order_report_keys = [ 'amount' => 'purchase_order.amount', 'balance' => 'purchase_order.balance', @@ -352,6 +381,7 @@ class BaseExport 'vendor' => $value = $this->resolveVendorKey($parts[1], $entity, $transformer), 'vendor_contact' => $value = $this->resolveVendorContactKey($parts[1], $entity, $transformer), 'invoice' => $value = $this->resolveInvoiceKey($parts[1], $entity, $transformer), + 'recurring_invoice' => $value = $this->resolveInvoiceKey($parts[1], $entity, $transformer), '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), @@ -764,7 +794,7 @@ class BaseExport { $header = []; - nlog($this->input['report_keys']); + // nlog($this->input['report_keys']); foreach (array_merge($this->input['report_keys'], $this->forced_keys) as $value) { @@ -782,6 +812,11 @@ class BaseExport $key = array_search($value, $this->invoice_report_keys); } + if(!$key) { + $prefix = ctrans('texts.recurring_invoice')." "; + $key = array_search($value, $this->recurring_invoice_report_keys); + } + if(!$key) { $prefix = ctrans('texts.payment')." "; $key = array_search($value, $this->payment_report_keys); @@ -851,8 +886,6 @@ class BaseExport } } -// nlog($header); - return $header; } } diff --git a/app/Export/CSV/RecurringInvoiceExport.php b/app/Export/CSV/RecurringInvoiceExport.php index 877fd04710fc..6df397c64855 100644 --- a/app/Export/CSV/RecurringInvoiceExport.php +++ b/app/Export/CSV/RecurringInvoiceExport.php @@ -32,10 +32,10 @@ class RecurringInvoiceExport extends BaseExport 'amount' => 'amount', 'balance' => 'balance', 'client' => 'client_id', - 'custom_surcharge1' => 'custom_surcharge1', - 'custom_surcharge2' => 'custom_surcharge2', - 'custom_surcharge3' => 'custom_surcharge3', - 'custom_surcharge4' => 'custom_surcharge4', + // 'custom_surcharge1' => 'custom_surcharge1', + // 'custom_surcharge2' => 'custom_surcharge2', + // 'custom_surcharge3' => 'custom_surcharge3', + // 'custom_surcharge4' => 'custom_surcharge4', 'custom_value1' => 'custom_value1', 'custom_value2' => 'custom_value2', 'custom_value3' => 'custom_value3', @@ -65,7 +65,8 @@ class RecurringInvoiceExport extends BaseExport 'currency' => 'currency_id', 'vendor' => 'vendor_id', 'project' => 'project_id', - 'frequency' => 'frequency_id' + 'frequency_id' => 'frequency_id', + 'next_send_date' => 'next_send_date' ]; private array $decorate_keys = [ @@ -126,11 +127,22 @@ class RecurringInvoiceExport extends BaseExport foreach (array_values($this->input['report_keys']) as $key) { $keyval = array_search($key, $this->entity_keys); + if(!$keyval) { + $keyval = array_search(str_replace("recurring_invoice.", "", $key), $this->entity_keys) ?? $key; + } + + if(!$keyval) { + $keyval = $key; + } + if (array_key_exists($key, $transformed_invoice)) { $entity[$keyval] = $transformed_invoice[$key]; + } elseif (array_key_exists($keyval, $transformed_invoice)) { + $entity[$keyval] = $transformed_invoice[$keyval]; } else { - $entity[$keyval] = ''; + $entity[$keyval] = $this->resolveKey($keyval, $invoice, $this->invoice_transformer); } + } return $this->decorateAdvancedFields($invoice, $entity); @@ -162,7 +174,9 @@ class RecurringInvoiceExport extends BaseExport $entity['vendor'] = $invoice->vendor ? $invoice->vendor->name : ''; } - $entity['frequency'] = $invoice->frequencyForKey($invoice->frequency_id); + if (in_array('recurring_invoice.frequency_id', $this->input['report_keys']) || in_array('frequency_id', $this->input['report_keys'])) { + $entity['frequency_id'] = $invoice->frequencyForKey($invoice->frequency_id); + } return $entity; } diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 14abc9ff5a73..c46ee9eb5c92 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -411,6 +411,47 @@ class ReportCsvGenerationTest extends TestCase } + public function testRecurringInvoiceCustomColumnsCsvGeneration() + { + + \App\Models\RecurringInvoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'frequency_id' => 1, + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","recurring_invoice.number","recurring_invoice.amount", "recurring_invoice.frequency_id"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/recurring_invoices', $data); + + $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')); + + } + + public function testInvoiceItemsCustomColumnsCsvGeneration() { @@ -983,7 +1024,7 @@ class ReportCsvGenerationTest extends TestCase $this->assertEquals('20', $this->getFirstValueByColumn($csv, 'Tax Rate 2')); $this->assertEquals('Tax 3', $this->getFirstValueByColumn($csv, 'Tax Name 3')); $this->assertEquals('30', $this->getFirstValueByColumn($csv, 'Tax Rate 3')); - $this->assertEquals('Daily', $this->getFirstValueByColumn($csv, 'Frequency')); + $this->assertEquals('Daily', $this->getFirstValueByColumn($csv, 'How Often')); } From 9fc9a0fd785e2cae0a2ab584884f6315546dc0bb Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 16:42:39 +1000 Subject: [PATCH 26/32] Add signature date to html variables --- app/Utils/HtmlEngine.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 27451e68d1f6..621568d2d0f2 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -583,8 +583,11 @@ class HtmlEngine if ($this->settings->signature_on_pdf) { $data['$contact.signature'] = ['value' => $this->invitation->signature_base64, 'label' => ctrans('texts.signature')]; + $data['$contact.signature_date'] = ['value' => $this->translateDate($this->invitation->signature_date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.date')]; + } else { $data['$contact.signature'] = ['value' => '', 'label' => '']; + $data['$contact.signature_date'] = ['value' => '', 'label' => ctrans('texts.date')]; } $data['$thanks'] = ['value' => '', 'label' => ctrans('texts.thanks')]; From a2b0a13c1f1c3d127e88b95f684b67a4b864024e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 18:21:12 +1000 Subject: [PATCH 27/32] Additional tests for report generation --- app/Export/CSV/BaseExport.php | 40 ++++- app/Export/CSV/TaskExport.php | 96 +++++------- app/Transformers/TaskTransformer.php | 12 ++ phpstan.neon | 2 +- .../Export/ReportCsvGenerationTest.php | 147 +++++++++++++++++- 5 files changed, 229 insertions(+), 68 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index ab2873a965a7..d11c9ef72c7e 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 fa60698e4792..387bda902043 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 028e2bf44ef4..d2080b6a4500 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 cebc91d3270e..b5b004a232a9 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 c46ee9eb5c92..c8f5eb79e243 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')); From 3b9c82d45686118eb0c44f43c33950b2afd42927 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 18:30:13 +1000 Subject: [PATCH 28/32] Vendor custom columns --- app/Export/CSV/VendorExport.php | 2 +- .../Export/ReportCsvGenerationTest.php | 93 ++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/app/Export/CSV/VendorExport.php b/app/Export/CSV/VendorExport.php index dfc49f86daa5..efc8bf32134e 100644 --- a/app/Export/CSV/VendorExport.php +++ b/app/Export/CSV/VendorExport.php @@ -114,7 +114,7 @@ class VendorExport extends BaseExport private function buildRow(Vendor $vendor) :array { - $transformed_contact = false; + $transformed_contact = []; $transformed_vendor = $this->vendor_transformer->transform($vendor); diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index c8f5eb79e243..e11adfb447f4 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -165,10 +165,101 @@ class ReportCsvGenerationTest extends TestCase } + public function testVendorCsvGeneration() + { + + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + 'city' => 'city', + 'address1' => 'address1', + 'address2' => 'address2', + 'postal_code' => 'postal_code', + 'phone' => 'work_phone', + 'private_notes' => 'private_notes', + 'public_notes' => 'public_notes', + 'website' => 'website', + 'number' => '1234', + ] + ); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + // 'report_keys' => ["vendor.name","purchase_order.number","purchase_order.amount", "item.quantity", "item.cost", "item.line_total", "item.discount", "item.notes", "item.product_key", "item.custom_value1", "item.tax_name1", "item.tax_rate1",], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/vendors', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Number')); + $this->assertEquals('city', $this->getFirstValueByColumn($csv, 'City')); + $this->assertEquals('address1', $this->getFirstValueByColumn($csv, 'Street')); + $this->assertEquals('address2', $this->getFirstValueByColumn($csv, 'Apt/Suite')); + $this->assertEquals('postal_code', $this->getFirstValueByColumn($csv, 'Postal Code')); + $this->assertEquals('work_phone', $this->getFirstValueByColumn($csv, 'Phone')); + $this->assertEquals('private_notes', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('public_notes', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('website', $this->getFirstValueByColumn($csv, 'Website')); + + } + + public function testVendorCustomColumnCsvGeneration() + { + + \App\Models\Vendor::query()->cursor()->each(function ($t) { + $t->forceDelete(); + }); + + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + 'city' => 'city', + 'address1' => 'address1', + 'address2' => 'address2', + 'postal_code' => 'postal_code', + 'phone' => 'work_phone', + 'private_notes' => 'private_notes', + 'public_notes' => 'public_notes', + 'website' => 'website', + 'number' => '1234', + ] + ); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["vendor.name", "vendor.city", "vendor.number"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/vendors', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Number')); + $this->assertEquals('city', $this->getFirstValueByColumn($csv, 'City')); + } + + public function testTaskCustomColumnsCsvGeneration() { - $invoice = \App\Models\Invoice::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, From 5fff511869a67caaa9c7763ab0223a98bbfc3c83 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 18 Jul 2023 19:54:03 +1000 Subject: [PATCH 29/32] Additional filter for expenses --- app/Filters/ExpenseFilters.php | 11 ++++++----- phpstan.neon | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/Filters/ExpenseFilters.php b/app/Filters/ExpenseFilters.php index b057ad3f27f9..4475973e3e8f 100644 --- a/app/Filters/ExpenseFilters.php +++ b/app/Filters/ExpenseFilters.php @@ -32,11 +32,12 @@ class ExpenseFilters extends QueryFilters } return $this->builder->where(function ($query) use ($filter) { - $query->where('public_notes', 'like', '%'.$filter.'%') - ->orWhere('custom_value1', 'like', '%'.$filter.'%') - ->orWhere('custom_value2', 'like', '%'.$filter.'%') - ->orWhere('custom_value3', 'like', '%'.$filter.'%') - ->orWhere('custom_value4', 'like', '%'.$filter.'%'); + $query->where('number', 'like', '%'.$filter.'%') + ->orWhere('public_notes', 'like', '%'.$filter.'%') + ->orWhere('custom_value1', 'like', '%'.$filter.'%') + ->orWhere('custom_value2', 'like', '%'.$filter.'%') + ->orWhere('custom_value3', 'like', '%'.$filter.'%') + ->orWhere('custom_value4', 'like', '%'.$filter.'%'); }); } diff --git a/phpstan.neon b/phpstan.neon index b5b004a232a9..d111b1c05f7a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,9 +4,9 @@ includes: parameters: treatPhpDocTypesAsCertain: false parallel: - jobSize: 5 + jobSize: 20 maximumNumberOfProcesses: 1 - processTimeout: 600.0 + processTimeout: 60.0 ignoreErrors: - '#Call to an undefined method .*badMethod\(\)#' - '#Call to an undefined method Illuminate\Database\Eloquent\Builder::exclude#' From a028456a09a855835731fbc8a966e6978b508a66 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 19 Jul 2023 08:01:47 +1000 Subject: [PATCH 30/32] Add filters for task start date --- app/Repositories/TaskRepository.php | 24 ++++++++++++--- ..._214607_add_start_date_column_to_tasks.php | 29 +++++++++++++++++++ tests/Feature/TaskApiTest.php | 16 ++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 database/migrations/2023_07_18_214607_add_start_date_column_to_tasks.php diff --git a/app/Repositories/TaskRepository.php b/app/Repositories/TaskRepository.php index 387979342aeb..be1f17e4911d 100644 --- a/app/Repositories/TaskRepository.php +++ b/app/Repositories/TaskRepository.php @@ -101,9 +101,6 @@ class TaskRepository extends BaseRepository $key_values = array_column($time_log, 0); array_multisort($key_values, SORT_ASC, $time_log); - // array_multisort($time_log); - // ksort($time_log); - if (isset($data['action'])) { if ($data['action'] == 'start') { $task->is_running = true; @@ -121,8 +118,12 @@ class TaskRepository extends BaseRepository $task->is_running = $data['is_running'] ? 1 : 0; } + $task->calculated_start_date = $this->harvestStartDate($time_log); + $task->time_log = json_encode($time_log); + + $task->saveQuietly(); if (array_key_exists('documents', $data)) { @@ -132,6 +133,17 @@ class TaskRepository extends BaseRepository return $task; } + private function harvestStartDate($time_log) + { + + if(isset($time_log[0][0])){ + return \Carbon\Carbon::createFromTimestamp($time_log[0][0]); + } + + return null; + + } + /** * Store tasks in bulk. * @@ -199,8 +211,12 @@ class TaskRepository extends BaseRepository if (strlen($task->time_log) < 5) { $log = []; - $log = array_merge($log, [[time(), 0]]); + $start_time = time(); + + $log = array_merge($log, [[$start_time, 0]]); $task->time_log = json_encode($log); + $task->calculated_start_date = \Carbon\Carbon::createFromTimestamp($start_time); + $task->saveQuietly(); } diff --git a/database/migrations/2023_07_18_214607_add_start_date_column_to_tasks.php b/database/migrations/2023_07_18_214607_add_start_date_column_to_tasks.php new file mode 100644 index 000000000000..ad988bd747ef --- /dev/null +++ b/database/migrations/2023_07_18_214607_add_start_date_column_to_tasks.php @@ -0,0 +1,29 @@ +date('calculated_start_date')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + } +}; diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php index 236f2fa66291..1651954bb121 100644 --- a/tests/Feature/TaskApiTest.php +++ b/tests/Feature/TaskApiTest.php @@ -31,6 +31,8 @@ class TaskApiTest extends TestCase use DatabaseTransactions; use MockAccountData; + private $faker; + protected function setUp() :void { parent::setUp(); @@ -100,6 +102,20 @@ class TaskApiTest extends TestCase } } + public function testStartDate() + { + $x = []; + + $this->assertFalse(isset($x[0][0])); + + $x[0][0] = 'a'; + + $this->assertTrue(isset($x[0][0])); + + $this->assertNotNull(\Carbon\Carbon::createFromTimestamp($x[0][0])); + + } + public function testMultiSortArray() { From fd9972f18bbffb9909407260b018d9127baca32f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 19 Jul 2023 08:32:10 +1000 Subject: [PATCH 31/32] Update email delays for quota notifications --- app/Models/Account.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Account.php b/app/Models/Account.php index e6a5701f558c..46ba1c330ace 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -554,7 +554,7 @@ class Account extends BaseModel $nmo->to_user = $this->companies()->first()->owner(); NinjaMailerJob::dispatch($nmo, true); - Cache::put("throttle_notified:{$this->key}", true, 60 * 24); + Cache::put("throttle_notified:{$this->key}", true, 60 * 60 * 24); if (config('ninja.notification.slack')) { $this->companies()->first()->notification(new EmailQuotaNotification($this))->ninja(); From cd12d79786b428c34ae964c5b39cd1e70edf5229 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 19 Jul 2023 08:49:57 +1000 Subject: [PATCH 32/32] minor fixes --- phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index d111b1c05f7a..2372c3d259ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,7 @@ includes: parameters: treatPhpDocTypesAsCertain: false parallel: - jobSize: 20 + jobSize: 10 maximumNumberOfProcesses: 1 processTimeout: 60.0 ignoreErrors: