From 84843f465fab798a3d1608edf2bcd08fb69105fb Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 4 Feb 2022 13:50:07 +1100 Subject: [PATCH 1/6] Fixes for tests --- tests/Unit/InvoiceItemTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Unit/InvoiceItemTest.php b/tests/Unit/InvoiceItemTest.php index 7b6f8e5621c6..e5af07f11dc9 100644 --- a/tests/Unit/InvoiceItemTest.php +++ b/tests/Unit/InvoiceItemTest.php @@ -208,7 +208,9 @@ class InvoiceItemTest extends TestCase $item->cost = 10; $item->is_amount_discount = true; $item->discount = 2.521254522145214511; + $item->tax_name1 = "GST"; $item->tax_rate1 = 10; + $item->tax_name2 = "VAT"; $item->tax_rate2 = 17.5; $this->invoice->line_items = [$item]; From 8acf738197dda1466789d6233e27b4a8b48edfe8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 4 Feb 2022 15:06:02 +1100 Subject: [PATCH 2/6] Fixes for tests --- tests/Unit/InvoiceItemV2Test.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Unit/InvoiceItemV2Test.php b/tests/Unit/InvoiceItemV2Test.php index 2b82216316d7..28b0cd2f5e4f 100644 --- a/tests/Unit/InvoiceItemV2Test.php +++ b/tests/Unit/InvoiceItemV2Test.php @@ -251,7 +251,9 @@ class InvoiceItemV2Test extends TestCase $item->cost = 10; $item->is_amount_discount = true; $item->discount = 2.521254522145214511; + $item->tax_name1 = 'GST'; $item->tax_rate1 = 10; + $item->tax_name2 = 'VAT'; $item->tax_rate2 = 17.5; $this->invoice->line_items = [$item]; From 5897a4e7497c76c6756638b9cec31e50d6b64537 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 6 Feb 2022 13:46:19 +1100 Subject: [PATCH 3/6] Fixes for allowing a deleted invoice to be marked as sent --- app/Services/Invoice/MarkSent.php | 4 +-- config/querydetector.php | 2 +- tests/Feature/InvoiceTest.php | 58 +++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/app/Services/Invoice/MarkSent.php b/app/Services/Invoice/MarkSent.php index 40a851f6dd09..5c7c53200665 100644 --- a/app/Services/Invoice/MarkSent.php +++ b/app/Services/Invoice/MarkSent.php @@ -33,8 +33,8 @@ class MarkSent extends AbstractService public function run() { - /* Return immediately if status is not draft */ - if ($this->invoice && $this->invoice->fresh()->status_id != Invoice::STATUS_DRAFT) { + /* Return immediately if status is not draft or invoice has been deleted */ + if ($this->invoice && ($this->invoice->fresh()->status_id != Invoice::STATUS_DRAFT || $this->invoice->is_deleted)) { return $this->invoice; } diff --git a/config/querydetector.php b/config/querydetector.php index 5f8207ef22e8..72b3fc33ada1 100644 --- a/config/querydetector.php +++ b/config/querydetector.php @@ -11,7 +11,7 @@ return [ * Threshold level for the N+1 query detection. If a relation query will be * executed more then this amount, the detector will notify you about it. */ - 'threshold' => (int) env('QUERY_DETECTOR_THRESHOLD', 1), + 'threshold' => (int) env('QUERY_DETECTOR_THRESHOLD', 3), /* * Here you can whitelist model relations. diff --git a/tests/Feature/InvoiceTest.php b/tests/Feature/InvoiceTest.php index 87d37188f2f1..d57f588ac8e6 100644 --- a/tests/Feature/InvoiceTest.php +++ b/tests/Feature/InvoiceTest.php @@ -10,9 +10,11 @@ */ namespace Tests\Feature; +use App\Helpers\Invoice\InvoiceSum; use App\Models\Client; use App\Models\ClientContact; use App\Models\Invoice; +use App\Repositories\InvoiceRepository; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -43,6 +45,62 @@ class InvoiceTest extends TestCase $this->makeTestData(); } + public function testMarkingDeletedInvoiceAsSent() + { + + Client::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id])->each(function ($c) { + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $c->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + ]); + + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $c->id, + 'company_id' => $this->company->id, + ]); + }); + + $client = Client::all()->first(); + + $invoice = Invoice::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->client->id]); + $invoice->status_id = Invoice::STATUS_DRAFT; + + $invoice->line_items = $this->buildLineItems(); + $invoice->uses_inclusive_taxes = false; + $invoice->tax_rate1 = 0; + $invoice->tax_rate2 = 0; + $invoice->tax_rate3 = 0; + $invoice->discount = 0; + + $invoice->save(); + + $invoice_calc = new InvoiceSum($invoice); + $invoice_calc->build(); + + $invoice = $invoice_calc->getInvoice(); + $invoice->save(); + + $this->assertEquals(Invoice::STATUS_DRAFT, $invoice->status_id); + $this->assertEquals(10, $invoice->amount); + $this->assertEquals(0, $invoice->balance); + + $invoice_repository = new InvoiceRepository(); + $invoice = $invoice_repository->delete($invoice); + + + $this->assertEquals(10, $invoice->amount); + $this->assertEquals(0, $invoice->balance); + $this->assertTrue($invoice->is_deleted); + + $invoice->service()->markSent()->save(); + + $this->assertEquals(0, $invoice->balance); + + } + public function testInvoiceList() { Client::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id])->each(function ($c) { From 35c80583c48efd2b76c3809ba5e65cba0297538b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 7 Feb 2022 12:31:14 +1100 Subject: [PATCH 4/6] Fixes for parsing markdown in pdf tables --- .../Designs/Utilities/DesignHelpers.php | 21 ++++++++++--------- app/Services/PdfMaker/PdfMaker.php | 1 + app/Services/PdfMaker/PdfMakerUtilities.php | 4 +++- composer.json | 1 - 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php b/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php index 82bbc3db684f..f2f61cc68240 100644 --- a/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php +++ b/app/Services/PdfMaker/Designs/Utilities/DesignHelpers.php @@ -382,22 +382,23 @@ document.addEventListener('DOMContentLoaded', function() { return $converter->convertToHtml($markdown); } - public function processMarkdownOnLineItems(array &$items): void - { - foreach ($items as $key => $item) { - foreach ($item as $variable => $value) { - $item[$variable] = DesignHelpers::parseMarkdownToHtml($value ?? ''); - } + // public function processMarkdownOnLineItems(array &$items): void + // { + // foreach ($items as $key => $item) { + // foreach ($item as $variable => $value) { + // $item[$variable] = DesignHelpers::parseMarkdownToHtml($value ?? ''); + // } - $items[$key] = $item; - } - } + // $items[$key] = $item; + // } + // } public function processNewLines(array &$items): void { foreach ($items as $key => $item) { foreach ($item as $variable => $value) { - $item[$variable] = nl2br($value); + // $item[$variable] = nl2br($value, true); + $item[$variable] = str_replace( "\n", '
', $value); } $items[$key] = $item; diff --git a/app/Services/PdfMaker/PdfMaker.php b/app/Services/PdfMaker/PdfMaker.php index ec492cf8e5a3..a38cf18e61ef 100644 --- a/app/Services/PdfMaker/PdfMaker.php +++ b/app/Services/PdfMaker/PdfMaker.php @@ -51,6 +51,7 @@ class PdfMaker $this->commonmark = new CommonMarkConverter([ 'allow_unsafe_links' => false, + // 'html_input' => 'allow', ]); } diff --git a/app/Services/PdfMaker/PdfMakerUtilities.php b/app/Services/PdfMaker/PdfMakerUtilities.php index ab72d76d3745..06817df0e949 100644 --- a/app/Services/PdfMaker/PdfMakerUtilities.php +++ b/app/Services/PdfMaker/PdfMakerUtilities.php @@ -92,7 +92,9 @@ trait PdfMakerUtilities $contains_html = false; if ($child['element'] !== 'script') { - if (array_key_exists('process_markdown', $this->data) && $this->data['process_markdown']) { + if (array_key_exists('process_markdown', $this->data) && array_key_exists('content', $child) && $this->data['process_markdown']) { + + $child['content'] = str_replace("
", "\r", $child['content']); $child['content'] = $this->commonmark->convertToHtml($child['content'] ?? ''); } } diff --git a/composer.json b/composer.json index f58ba6ad8c22..877931817fe1 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,6 @@ "eway/eway-rapid-php": "^1.3", "fakerphp/faker": "^1.14", "fideloper/proxy": "^4.2", - "firebase/php-jwt": "^5", "fruitcake/laravel-cors": "^2.0", "gocardless/gocardless-pro": "^4.12", "google/apiclient": "^2.7", From e09f160877b5f46538a8eb00c409fef586c07ccb Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 7 Feb 2022 14:18:10 +1100 Subject: [PATCH 5/6] Process reserved keywords in Recurring Expenses --- .../RecurringExpenseToExpenseFactory.php | 188 +++++++++++++++++- 1 file changed, 186 insertions(+), 2 deletions(-) diff --git a/app/Factory/RecurringExpenseToExpenseFactory.php b/app/Factory/RecurringExpenseToExpenseFactory.php index 466baea038e9..c9d1318087ee 100644 --- a/app/Factory/RecurringExpenseToExpenseFactory.php +++ b/app/Factory/RecurringExpenseToExpenseFactory.php @@ -13,6 +13,10 @@ namespace App\Factory; use App\Models\Expense; use App\Models\RecurringExpense; +use App\Utils\Helpers; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Str; class RecurringExpenseToExpenseFactory { @@ -21,6 +25,7 @@ class RecurringExpenseToExpenseFactory $expense = new Expense(); $expense->user_id = $recurring_expense->user_id; $expense->assigned_user_id = $recurring_expense->assigned_user_id; + $expense->client_id = $recurring_expense->client_id; $expense->vendor_id = $recurring_expense->vendor_id; $expense->invoice_id = $recurring_expense->invoice_id; $expense->currency_id = $recurring_expense->currency_id; @@ -39,8 +44,12 @@ class RecurringExpenseToExpenseFactory $expense->payment_date = $recurring_expense->payment_date; $expense->amount = $recurring_expense->amount; $expense->foreign_amount = $recurring_expense->foreign_amount ?: 0; - $expense->private_notes = $recurring_expense->private_notes; - $expense->public_notes = $recurring_expense->public_notes; + // $expense->private_notes = $recurring_expense->private_notes; + // $expense->public_notes = $recurring_expense->public_notes; + + $expense->public_notes = self::transformObject($recurring_expense->public_notes, $recurring_expense); + $expense->private_notes = self::transformObject($recurring_expense->private_notes, $recurring_expense); + $expense->transaction_reference = $recurring_expense->transaction_reference; $expense->custom_value1 = $recurring_expense->custom_value1; $expense->custom_value2 = $recurring_expense->custom_value2; @@ -59,4 +68,179 @@ class RecurringExpenseToExpenseFactory return $expense; } + + public static function transformObject(?string $value, $recurring_expense): ?string + { + if(!$value) + return ''; + + if($recurring_expense->client){ + + $locale = $recurring_expense->client->locale(); + $date_format = $recurring_expense->client->date_format(); + + } + else { + $locale = $recurring_expense->company->locale(); + + $date_formats = Cache::get('date_formats'); + + $date_format = $date_formats->filter(function ($item) use($recurring_expense){ + return $item->id == $recurring_expense->company->settings->date_format_id; + })->first()->format; + } + + Carbon::setLocale($locale); + + $replacements = [ + 'literal' => [ + ':MONTH' => Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F'), + ':YEAR' => now()->year, + ':QUARTER' => 'Q' . now()->quarter, + ':WEEK_BEFORE' => \sprintf( + '%s %s %s', + Carbon::now()->subDays(7)->translatedFormat($date_format), + ctrans('texts.to'), + Carbon::now()->translatedFormat($date_format) + ), + ':WEEK_AHEAD' => \sprintf( + '%s %s %s', + Carbon::now()->addDays(7)->translatedFormat($date_format), + ctrans('texts.to'), + Carbon::now()->addDays(14)->translatedFormat($date_format) + ), + ':WEEK' => \sprintf( + '%s %s %s', + Carbon::now()->translatedFormat($date_format), + ctrans('texts.to'), + Carbon::now()->addDays(7)->translatedFormat($date_format) + ), + ], + 'raw' => [ + ':MONTH' => now()->month, + ':YEAR' => now()->year, + ':QUARTER' => now()->quarter, + ], + 'ranges' => [ + 'MONTHYEAR' => Carbon::createFromDate(now()->year, now()->month), + ], + 'ranges_raw' => [ + 'MONTH' => now()->month, + 'YEAR' => now()->year, + ], + ]; + + // First case, with ranges. + preg_match_all('/\[(.*?)]/', $value, $ranges); + + $matches = array_shift($ranges); + + foreach ($matches as $match) { + if (!Str::contains($match, '|')) { + continue; + } + + if (Str::contains($match, '|')) { + $parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ] + + $left = substr($parts[0], 1); // 'MONTH' + $right = substr($parts[1], 0, -1); // MONTH+2 + + // If left side is not part of replacements, skip. + if (!array_key_exists($left, $replacements['ranges'])) { + continue; + } + + $_left = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y'); + $_right = ''; + + // If right side doesn't have any calculations, replace with raw ranges keyword. + if (!Str::contains($right, ['-', '+', '/', '*'])) { + $_right = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y'); + } + + // If right side contains one of math operations, calculate. + if (Str::contains($right, ['+'])) { + $operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $right, $_matches); + + $_operation = array_shift($_matches)[0]; // + - + + $_value = explode($_operation, $right); // [MONTHYEAR, 4] + + $_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y'); + } + + $replacement = sprintf('%s to %s', $_left, $_right); + + $value = preg_replace( + sprintf('/%s/', preg_quote($match)), $replacement, $value, 1 + ); + } + } + + + // Second case with more common calculations. + preg_match_all('/:([^:\s]+)/', $value, $common); + + $matches = array_shift($common); + + foreach ($matches as $match) { + $matches = collect($replacements['literal'])->filter(function ($value, $key) use ($match) { + return Str::startsWith($match, $key); + }); + + if ($matches->count() === 0) { + continue; + } + + if (!Str::contains($match, ['-', '+', '/', '*'])) { + $value = preg_replace( + sprintf('/%s/', $matches->keys()->first()), $replacements['literal'][$matches->keys()->first()], $value, 1 + ); + } + + if (Str::contains($match, ['-', '+', '/', '*'])) { + $operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $match, $_matches); + + $_operation = array_shift($_matches)[0]; + + $_value = explode($_operation, $match); // [:MONTH, 4] + + $raw = strtr($matches->keys()->first(), $replacements['raw']); // :MONTH => 1 + + $number = $res = preg_replace("/[^0-9]/", '', $_value[1]); // :MONTH+1. || :MONTH+2! => 1 || 2 + + $target = "/{$matches->keys()->first()}\\{$_operation}{$number}/"; // /:$KEYWORD\\$OPERATION$VALUE => /:MONTH\\+1 + + $output = (int) $raw + (int)$_value[1]; + + if ($operation == '+') { + $output = (int) $raw + (int)$_value[1]; // 1 (:MONTH) + 4 + } + + if ($_operation == '-') { + $output = (int)$raw - (int)$_value[1]; // 1 (:MONTH) - 4 + } + + if ($_operation == '/' && (int)$_value[1] != 0) { + $output = (int)$raw / (int)$_value[1]; // 1 (:MONTH) / 4 + } + + if ($_operation == '*') { + $output = (int)$raw * (int)$_value[1]; // 1 (:MONTH) * 4 + } + + if ($matches->keys()->first() == ':MONTH') { + $output = \Carbon\Carbon::create()->month($output)->translatedFormat('F'); + } + + $value = preg_replace( + $target, $output, $value, 1 + ); + } + } + + return $value; + } + } From fd84e534d90c156046e98cd81a0b72cdae170408 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 7 Feb 2022 19:01:05 +1100 Subject: [PATCH 6/6] Add currency conversion column --- app/Models/Company.php | 1 + app/Models/Invoice.php | 1 + app/Transformers/CompanyTransformer.php | 1 + ...currency_conversion_to_companies_table.php | 30 +++++++++++++++++++ 4 files changed, 33 insertions(+) create mode 100644 database/migrations/2022_02_06_091629_add_client_currency_conversion_to_companies_table.php diff --git a/app/Models/Company.php b/app/Models/Company.php index ec7497f1ebd1..40dae5dc15a2 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -97,6 +97,7 @@ class Company extends BaseModel 'use_comma_as_decimal_place', 'report_include_drafts', 'client_registration_fields', + 'convert_rate_to_client', ]; protected $hidden = [ diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 93ada7f481c5..00dbe7962327 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -90,6 +90,7 @@ class Invoice extends BaseModel 'subscription_id', 'auto_bill_enabled', 'uses_inclusive_taxes', + 'vendor_id', ]; protected $casts = [ diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 9fcd7abf44f9..453f1c3c8978 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -166,6 +166,7 @@ class CompanyTransformer extends EntityTransformer 'use_comma_as_decimal_place' => (bool) $company->use_comma_as_decimal_place, 'report_include_drafts' => (bool) $company->report_include_drafts, 'client_registration_fields' => (array) $company->client_registration_fields, + 'convert_rate_to_client' => (bool) $company->convert_rate_to_client, ]; } diff --git a/database/migrations/2022_02_06_091629_add_client_currency_conversion_to_companies_table.php b/database/migrations/2022_02_06_091629_add_client_currency_conversion_to_companies_table.php new file mode 100644 index 000000000000..08134aba1e73 --- /dev/null +++ b/database/migrations/2022_02_06_091629_add_client_currency_conversion_to_companies_table.php @@ -0,0 +1,30 @@ +boolean('convert_rate_to_client')->default(true); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + + } +}