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;
+ }
+
}
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/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/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/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/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",
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/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()
+ {
+
+ }
+}
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) {
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];
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];