From 9b7285ea84959a1794456b0ef0356bb3050f9bea Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 28 Sep 2022 07:35:57 +0200 Subject: [PATCH] Expand search and add operators. --- .../Extensions/AttachmentCollection.php | 208 ++++++++++++ .../Collector/Extensions/MetaCollection.php | 173 ++++++++++ .../Collector/GroupCollectorInterface.php | 139 +++++++++ app/Support/ParseDateString.php | 5 +- app/Support/Search/OperatorQuerySearch.php | 295 +++++++++++------- app/Support/Twig/General.php | 3 +- resources/lang/en_US/firefly.php | 160 +++++++++- 7 files changed, 865 insertions(+), 118 deletions(-) diff --git a/app/Helpers/Collector/Extensions/AttachmentCollection.php b/app/Helpers/Collector/Extensions/AttachmentCollection.php index 50d1b5738b..a984ba8994 100644 --- a/app/Helpers/Collector/Extensions/AttachmentCollection.php +++ b/app/Helpers/Collector/Extensions/AttachmentCollection.php @@ -62,6 +62,32 @@ trait AttachmentCollection return $this; } + /** + * @param string $name + * @return GroupCollectorInterface + */ + public function attachmentNameDoesNotContain(string $name): GroupCollectorInterface + { + $this->hasAttachments(); + $this->withAttachmentInformation(); + $filter = function (int $index, array $object) use ($name): bool { + /** @var array $transaction */ + foreach ($object['transactions'] as $transaction) { + /** @var array $attachment */ + foreach ($transaction['attachments'] as $attachment) { + $result = !str_contains(strtolower($attachment['filename']), strtolower($name)) && !str_contains(strtolower($attachment['title']), strtolower($name)); + if (true === $result) { + return true; + } + } + } + return false; + }; + $this->postFilters[] = $filter; + + return $this; + } + /** * Has attachments * @@ -135,6 +161,32 @@ trait AttachmentCollection return $this; } + /** + * @param string $name + * @return GroupCollectorInterface + */ + public function attachmentNameDoesNotEnd(string $name): GroupCollectorInterface + { + $this->hasAttachments(); + $this->withAttachmentInformation(); + $filter = function (int $index, array $object) use ($name): bool { + /** @var array $transaction */ + foreach ($object['transactions'] as $transaction) { + /** @var array $attachment */ + foreach ($transaction['attachments'] as $attachment) { + $result = !str_ends_with(strtolower($attachment['filename']), strtolower($name)) && !str_ends_with(strtolower($attachment['title']), strtolower($name)); + if (true === $result) { + return true; + } + } + } + return false; + }; + $this->postFilters[] = $filter; + + return $this; + } + /** * @param string $name * @return GroupCollectorInterface @@ -161,6 +213,32 @@ trait AttachmentCollection return $this; } + /** + * @param string $name + * @return GroupCollectorInterface + */ + public function attachmentNameIsNot(string $name): GroupCollectorInterface + { + $this->hasAttachments(); + $this->withAttachmentInformation(); + $filter = function (int $index, array $object) use ($name): bool { + /** @var array $transaction */ + foreach ($object['transactions'] as $transaction) { + /** @var array $attachment */ + foreach ($transaction['attachments'] as $attachment) { + $result = $attachment['filename'] !== $name && $attachment['title'] !== $name; + if (true === $result) { + return true; + } + } + } + return false; + }; + $this->postFilters[] = $filter; + + return $this; + } + /** * @param string $name * @return GroupCollectorInterface @@ -187,6 +265,32 @@ trait AttachmentCollection return $this; } + /** + * @param string $name + * @return GroupCollectorInterface + */ + public function attachmentNameDoesNotStart(string $name): GroupCollectorInterface + { + $this->hasAttachments(); + $this->withAttachmentInformation(); + $filter = function (int $index, array $object) use ($name): bool { + /** @var array $transaction */ + foreach ($object['transactions'] as $transaction) { + /** @var array $attachment */ + foreach ($transaction['attachments'] as $attachment) { + $result = !str_starts_with(strtolower($attachment['filename']), strtolower($name)) && !str_starts_with(strtolower($attachment['title']), strtolower($name)); + if (true === $result) { + return true; + } + } + } + return false; + }; + $this->postFilters[] = $filter; + + return $this; + } + /** * @param string $value * @return GroupCollectorInterface @@ -213,6 +317,32 @@ trait AttachmentCollection return $this; } + /** + * @param string $value + * @return GroupCollectorInterface + */ + public function attachmentNotesAreNot(string $value): GroupCollectorInterface + { + $this->hasAttachments(); + $this->withAttachmentInformation(); + $filter = function (int $index, array $object) use ($value): bool { + /** @var array $transaction */ + foreach ($object['transactions'] as $transaction) { + /** @var array $attachment */ + foreach ($transaction['attachments'] as $attachment) { + /** @var Attachment $object */ + $object = auth()->user()->attachments()->find($attachment['id']); + $notes = (string) $object->notes()?->first()?->text; + return $notes !== '' && $notes !== $value; + } + } + return false; + }; + $this->postFilters[] = $filter; + + return $this; + } + /** * @param string $value * @return GroupCollectorInterface @@ -239,6 +369,32 @@ trait AttachmentCollection return $this; } + /** + * @param string $value + * @return GroupCollectorInterface + */ + public function attachmentNotesDoNotContain(string $value): GroupCollectorInterface + { + $this->hasAttachments(); + $this->withAttachmentInformation(); + $filter = function (int $index, array $object) use ($value): bool { + /** @var array $transaction */ + foreach ($object['transactions'] as $transaction) { + /** @var array $attachment */ + foreach ($transaction['attachments'] as $attachment) { + /** @var Attachment $object */ + $object = auth()->user()->attachments()->find($attachment['id']); + $notes = (string) $object->notes()?->first()?->text; + return $notes !== '' && !str_contains(strtolower($notes), strtolower($value)); + } + } + return false; + }; + $this->postFilters[] = $filter; + + return $this; + } + /** * @param string $value * @return GroupCollectorInterface @@ -265,6 +421,32 @@ trait AttachmentCollection return $this; } + /** + * @param string $value + * @return GroupCollectorInterface + */ + public function attachmentNotesDoNotEnd(string $value): GroupCollectorInterface + { + $this->hasAttachments(); + $this->withAttachmentInformation(); + $filter = function (int $index, array $object) use ($value): bool { + /** @var array $transaction */ + foreach ($object['transactions'] as $transaction) { + /** @var array $attachment */ + foreach ($transaction['attachments'] as $attachment) { + /** @var Attachment $object */ + $object = auth()->user()->attachments()->find($attachment['id']); + $notes = (string) $object->notes()?->first()?->text; + return $notes !== '' && !str_ends_with(strtolower($notes), strtolower($value)); + } + } + return false; + }; + $this->postFilters[] = $filter; + + return $this; + } + /** * @param string $value * @return GroupCollectorInterface @@ -291,6 +473,32 @@ trait AttachmentCollection return $this; } + /** + * @param string $value + * @return GroupCollectorInterface + */ + public function attachmentNotesDoNotStart(string $value): GroupCollectorInterface + { + $this->hasAttachments(); + $this->withAttachmentInformation(); + $filter = function (int $index, array $object) use ($value): bool { + /** @var array $transaction */ + foreach ($object['transactions'] as $transaction) { + /** @var array $attachment */ + foreach ($transaction['attachments'] as $attachment) { + /** @var Attachment $object */ + $object = auth()->user()->attachments()->find($attachment['id']); + $notes = (string) $object->notes()?->first()?->text; + return $notes !== '' && !str_starts_with(strtolower($notes), strtolower($value)); + } + } + return false; + }; + $this->postFilters[] = $filter; + + return $this; + } + /** * Has attachments * diff --git a/app/Helpers/Collector/Extensions/MetaCollection.php b/app/Helpers/Collector/Extensions/MetaCollection.php index 15113475f8..4f94df0d79 100644 --- a/app/Helpers/Collector/Extensions/MetaCollection.php +++ b/app/Helpers/Collector/Extensions/MetaCollection.php @@ -135,6 +135,18 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function externalIdDoesNotContain(string $externalId): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $this->query->where('journal_meta.name', '=', 'external_id'); + $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $externalId)); + + return $this; + } + /** * Join table to get tag information. */ @@ -160,6 +172,18 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function externalIdDoesNotEnd(string $externalId): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $this->query->where('journal_meta.name', '=', 'external_id'); + $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s"', $externalId)); + + return $this; + } + /** * @inheritDoc */ @@ -172,6 +196,18 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function externalIdDoesNotStart(string $externalId): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $this->query->where('journal_meta.name', '=', 'external_id'); + $this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $externalId)); + + return $this; + } + /** * @param string $url * @return GroupCollectorInterface @@ -187,6 +223,23 @@ trait MetaCollection return $this; } + /** + * @param string $url + * @return GroupCollectorInterface + */ + public function externalUrlDoesNotContain(string $url): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $url = json_encode($url); + $url = str_replace('\\', '\\\\', trim($url, '"')); + $this->query->where('journal_meta.name', '=', 'external_url'); + $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $url)); + + return $this; + } + + + /** * @param string $url * @return GroupCollectorInterface @@ -202,6 +255,23 @@ trait MetaCollection return $this; } + /** + * @param string $url + * @return GroupCollectorInterface + */ + public function externalUrlDoesNotEnd(string $url): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $url = json_encode($url); + $url = str_replace('\\', '\\\\', ltrim($url, '"')); + $this->query->where('journal_meta.name', '=', 'external_url'); + $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s', $url)); + + return $this; + } + + + /** * @param string $url * @return GroupCollectorInterface @@ -219,6 +289,23 @@ trait MetaCollection return $this; } + /** + * @param string $url + * @return GroupCollectorInterface + */ + public function externalUrlDoesNotStart(string $url): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $url = json_encode($url); + $url = str_replace('\\', '\\\\', rtrim($url, '"')); + //var_dump($url); + + $this->query->where('journal_meta.name', '=', 'external_url'); + $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%s%%', $url)); + + return $this; + } + /** * Where has no tags. * @@ -275,6 +362,18 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function internalReferenceDoesNotContain(string $externalId): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $this->query->where('journal_meta.name', '=', 'internal_reference'); + $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $externalId)); + + return $this; + } + /** * @inheritDoc */ @@ -287,6 +386,18 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function internalReferenceDoesNotEnd(string $externalId): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $this->query->where('journal_meta.name', '=', 'internal_reference'); + $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s"', $externalId)); + + return $this; + } + /** * @inheritDoc */ @@ -299,6 +410,18 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function internalReferenceDoesNotStart(string $externalId): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $this->query->where('journal_meta.name', '=', 'internal_reference'); + $this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $externalId)); + + return $this; + } + /** * @param string $value * @@ -605,6 +728,19 @@ trait MetaCollection return $this; } + + /** + * @inheritDoc + */ + public function excludeExternalId(string $externalId): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $this->query->where('journal_meta.name', '=', 'external_id'); + $this->query->where('journal_meta.data', '!=', sprintf('%s', json_encode($externalId))); + + return $this; + } + /** * @inheritDoc */ @@ -617,6 +753,18 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function excludeExternalUrl(string $url): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $this->query->where('journal_meta.name', '=', 'external_url'); + $this->query->where('journal_meta.data', '!=', json_encode($url)); + + return $this; + } + /** * @inheritDoc */ @@ -630,6 +778,19 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function excludeInternalReference(string $internalReference): GroupCollectorInterface + { + $this->joinMetaDataTables(); + + $this->query->where('journal_meta.name', '=', 'internal_reference'); + $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $internalReference)); + + return $this; + } + /** * @inheritDoc */ @@ -642,6 +803,18 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function excludeRecurrenceId(string $recurringId): GroupCollectorInterface + { + $this->joinMetaDataTables(); + $this->query->where('journal_meta.name', '=', 'recurrence_id'); + $this->query->where('journal_meta.data', '!=', sprintf('%s', json_encode($recurringId))); + + return $this; + } + /** * Limit results to a specific tag. * diff --git a/app/Helpers/Collector/GroupCollectorInterface.php b/app/Helpers/Collector/GroupCollectorInterface.php index fb162d8203..f0cba4abf9 100644 --- a/app/Helpers/Collector/GroupCollectorInterface.php +++ b/app/Helpers/Collector/GroupCollectorInterface.php @@ -79,18 +79,42 @@ interface GroupCollectorInterface */ public function attachmentNameContains(string $name): GroupCollectorInterface; + /** + * @param string $name + * @return GroupCollectorInterface + */ + public function attachmentNameDoesNotContain(string $name): GroupCollectorInterface; + /** * @param string $name * @return GroupCollectorInterface */ public function attachmentNameEnds(string $name): GroupCollectorInterface; + /** + * @param string $name + * @return GroupCollectorInterface + */ + public function attachmentNameDoesNotEnd(string $name): GroupCollectorInterface; + /** * @param string $name * @return GroupCollectorInterface */ public function attachmentNameIs(string $name): GroupCollectorInterface; + /** + * @param string $name + * @return GroupCollectorInterface + */ + public function attachmentNameIsNot(string $name): GroupCollectorInterface; + + /** + * @param string $name + * @return GroupCollectorInterface + */ + public function attachmentNameDoesNotStart(string $name): GroupCollectorInterface; + /** * @param string $name * @return GroupCollectorInterface @@ -103,12 +127,24 @@ interface GroupCollectorInterface */ public function attachmentNotesAre(string $value): GroupCollectorInterface; + /** + * @param string $value + * @return GroupCollectorInterface + */ + public function attachmentNotesAreNot(string $value): GroupCollectorInterface; + /** * @param string $value * @return GroupCollectorInterface */ public function attachmentNotesContains(string $value): GroupCollectorInterface; + /** + * @param string $value + * @return GroupCollectorInterface + */ + public function attachmentNotesDoNotContain(string $value): GroupCollectorInterface; + /** * @param string $value * @return GroupCollectorInterface @@ -121,6 +157,18 @@ interface GroupCollectorInterface */ public function attachmentNotesStarts(string $value): GroupCollectorInterface; + /** + * @param string $value + * @return GroupCollectorInterface + */ + public function attachmentNotesDoNotEnd(string $value): GroupCollectorInterface; + + /** + * @param string $value + * @return GroupCollectorInterface + */ + public function attachmentNotesDoNotStart(string $value): GroupCollectorInterface; + /** * @param string $day * @return GroupCollectorInterface @@ -358,36 +406,76 @@ interface GroupCollectorInterface */ public function externalIdContains(string $externalId): GroupCollectorInterface; + /** + * @param string $externalId + * @return GroupCollectorInterface + */ + public function externalIdDoesNotContain(string $externalId): GroupCollectorInterface; + + + /** * @param string $externalId * @return GroupCollectorInterface */ public function externalIdEnds(string $externalId): GroupCollectorInterface; + /** + * @param string $externalId + * @return GroupCollectorInterface + */ + public function externalIdDoesNotEnd(string $externalId): GroupCollectorInterface; + /** * @param string $externalId * @return GroupCollectorInterface */ public function externalIdStarts(string $externalId): GroupCollectorInterface; + /** + * @param string $externalId + * @return GroupCollectorInterface + */ + public function externalIdDoesNotStart(string $externalId): GroupCollectorInterface; + + + /** * @param string $url * @return GroupCollectorInterface */ public function externalUrlContains(string $url): GroupCollectorInterface; + /** + * @param string $url + * @return GroupCollectorInterface + */ + public function externalUrlDoesNotContain(string $url): GroupCollectorInterface; + /** * @param string $url * @return GroupCollectorInterface */ public function externalUrlEnds(string $url): GroupCollectorInterface; + /** + * @param string $url + * @return GroupCollectorInterface + */ + public function externalUrlDoesNotEnd(string $url): GroupCollectorInterface; + /** * @param string $url * @return GroupCollectorInterface */ public function externalUrlStarts(string $url): GroupCollectorInterface; + /** + * @param string $url + * @return GroupCollectorInterface + */ + public function externalUrlDoesNotStart(string $url): GroupCollectorInterface; + /** * Ensure the search will find nothing at all, zero results. * @@ -477,18 +565,36 @@ interface GroupCollectorInterface */ public function internalReferenceContains(string $externalId): GroupCollectorInterface; + /** + * @param string $externalId + * @return GroupCollectorInterface + */ + public function internalReferenceDoesNotContain(string $externalId): GroupCollectorInterface; + /** * @param string $externalId * @return GroupCollectorInterface */ public function internalReferenceEnds(string $externalId): GroupCollectorInterface; + /** + * @param string $externalId + * @return GroupCollectorInterface + */ + public function internalReferenceDoesNotEnd(string $externalId): GroupCollectorInterface; + /** * @param string $externalId * @return GroupCollectorInterface */ public function internalReferenceStarts(string $externalId): GroupCollectorInterface; + /** + * @param string $externalId + * @return GroupCollectorInterface + */ + public function internalReferenceDoesNotStart(string $externalId): GroupCollectorInterface; + /** * Only journals that are reconciled. * @@ -876,12 +982,27 @@ interface GroupCollectorInterface */ public function setExternalId(string $externalId): GroupCollectorInterface; + /** + * Look for specific external ID's. + * + * @param string $externalId + * + * @return GroupCollectorInterface + */ + public function excludeExternalId(string $externalId): GroupCollectorInterface; + /** * @param string $url * @return GroupCollectorInterface */ public function setExternalUrl(string $url): GroupCollectorInterface; + /** + * @param string $url + * @return GroupCollectorInterface + */ + public function excludeExternalUrl(string $url): GroupCollectorInterface; + /** * Limit results to a specific foreign currency. * @@ -909,6 +1030,17 @@ interface GroupCollectorInterface */ public function setInternalReference(string $externalId): GroupCollectorInterface; + /** + * Look for specific external ID's. + * + * @param string $externalId + * + * @return GroupCollectorInterface + */ + public function excludeInternalReference(string $externalId): GroupCollectorInterface; + + + /** * Limit the result to a set of specific transaction journals. * @@ -1015,6 +1147,13 @@ interface GroupCollectorInterface */ public function setRecurrenceId(string $recurringId): GroupCollectorInterface; + /** + * @param string $recurringId + * + * @return GroupCollectorInterface + */ + public function excludeRecurrenceId(string $recurringId): GroupCollectorInterface; + /** * Search for words in descriptions. * diff --git a/app/Support/ParseDateString.php b/app/Support/ParseDateString.php index b04870b01b..5d06033896 100644 --- a/app/Support/ParseDateString.php +++ b/app/Support/ParseDateString.php @@ -33,7 +33,7 @@ use Log; */ class ParseDateString { - private $keywords + private array $keywords = [ 'today', 'yesterday', @@ -80,6 +80,7 @@ class ParseDateString */ public function parseDate(string $date): Carbon { + Log::debug(sprintf('parseDate("%s")', $date)); $date = strtolower($date); // parse keywords: if (in_array($date, $this->keywords, true)) { @@ -118,7 +119,7 @@ class ParseDateString return new Carbon(sprintf('%d-01-01', $date)); } - throw new FireflyException(sprintf('[d]Not a recognised date format: "%s"', $date)); + throw new FireflyException(sprintf('[d] Not a recognised date format: "%s"', $date)); } /** diff --git a/app/Support/Search/OperatorQuerySearch.php b/app/Support/Search/OperatorQuerySearch.php index 5b832dc8d4..5bd9f08386 100644 --- a/app/Support/Search/OperatorQuerySearch.php +++ b/app/Support/Search/OperatorQuerySearch.php @@ -190,7 +190,7 @@ class OperatorQuerySearch implements SearchInterface case Subquery::class: // loop all notes in subquery: foreach ($searchNode->getNodes() as $subNode) { // @phpstan-ignore-line - $this->handleSearchNode($subNode); // let's hope it's not too recursive! + $this->handleSearchNode($subNode); // let's hope it's not too recursive } break; case Word::class: @@ -254,8 +254,8 @@ class OperatorQuerySearch implements SearchInterface private function updateCollector(string $operator, string $value, bool $prohibited): bool { if ($prohibited) { - Log::debug(sprintf('Operator "%s" is now "%s"', $operator, '!' . $operator)); - $operator = sprintf('!%s', $operator); + Log::debug(sprintf('Operator "%s" is now "%s"', $operator, sprintf('-%s', $operator))); + $operator = sprintf('-%s', $operator); } Log::debug(sprintf('Now in updateCollector("%s", "%s")', $operator, $value)); @@ -278,97 +278,97 @@ class OperatorQuerySearch implements SearchInterface case 'account_is': $this->searchAccount($value, 3, 4); break; - case '!account_is': + case '-account_is': $this->searchAccount($value, 3, 4, true); break; case 'account_contains': $this->searchAccount($value, 3, 3); break; - case '!account_contains': + case '-account_contains': $this->searchAccount($value, 3, 3, true); break; case 'account_ends': $this->searchAccount($value, 3, 2); break; - case '!account_ends': + case '-account_ends': $this->searchAccount($value, 3, 2, true); break; case 'account_starts': $this->searchAccount($value, 3, 1); break; - case '!account_starts': + case '-account_starts': $this->searchAccount($value, 3, 1, true); break; case 'account_nr_is': $this->searchAccountNr($value, 3, 4); break; - case '!account_nr_is': + case '-account_nr_is': $this->searchAccountNr($value, 3, 4, true); break; case 'account_nr_contains': $this->searchAccountNr($value, 3, 3); break; - case '!account_nr_contains': + case '-account_nr_contains': $this->searchAccountNr($value, 3, 3, true); break; case 'account_nr_ends': $this->searchAccountNr($value, 3, 2); break; - case '!account_nr_ends': + case '-account_nr_ends': $this->searchAccountNr($value, 3, 2, true); break; case 'account_nr_starts': $this->searchAccountNr($value, 3, 1); break; - case '!account_nr_starts': + case '-account_nr_starts': $this->searchAccountNr($value, 3, 1, true); break; case 'source_account_starts': $this->searchAccount($value, 1, 1); break; - case '!source_account_starts': + case '-source_account_starts': $this->searchAccount($value, 1, 1, true); break; case 'source_account_ends': $this->searchAccount($value, 1, 2); break; - case '!source_account_ends': + case '-source_account_ends': $this->searchAccount($value, 1, 2, true); break; case 'source_account_is': $this->searchAccount($value, 1, 4); break; - case '!source_account_is': + case '-source_account_is': $this->searchAccount($value, 1, 4, true); break; case 'source_account_nr_starts': $this->searchAccountNr($value, 1, 1); break; - case '!source_account_nr_starts': + case '-source_account_nr_starts': $this->searchAccountNr($value, 1, 1, true); break; case 'source_account_nr_ends': $this->searchAccountNr($value, 1, 2); break; - case '!source_account_nr_ends': + case '-source_account_nr_ends': $this->searchAccountNr($value, 1, 2, true); break; case 'source_account_nr_is': $this->searchAccountNr($value, 1, 4); break; - case '!source_account_nr_is': + case '-source_account_nr_is': $this->searchAccountNr($value, 1, 4, true); break; case 'source_account_nr_contains': $this->searchAccountNr($value, 1, 3); break; - case '!source_account_nr_contains': + case '-source_account_nr_contains': $this->searchAccountNr($value, 1, 3, true); break; case 'source_account_contains': $this->searchAccount($value, 1, 3); break; - case '!source_account_contains': + case '-source_account_contains': $this->searchAccount($value, 1, 3, true); break; case 'source_account_id': @@ -381,7 +381,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!source_account_id': + case '-source_account_id': $account = $this->accountRepository->find((int) $value); if (null !== $account) { $this->collector->excludeSourceAccounts(new Collection([$account])); @@ -395,7 +395,7 @@ class OperatorQuerySearch implements SearchInterface $parts = explode(',', $value); $this->collector->setJournalIds($parts); break; - case '!journal_id': + case '-journal_id': $parts = explode(',', $value); $this->collector->excludeJournalIds($parts); break; @@ -403,56 +403,56 @@ class OperatorQuerySearch implements SearchInterface $parts = explode(',', $value); $this->collector->setIds($parts); break; - case '!id': + case '-id': $parts = explode(',', $value); $this->collector->excludeIds($parts); break; case 'destination_account_starts': $this->searchAccount($value, 2, 1); break; - case '!destination_account_starts': + case '-destination_account_starts': $this->searchAccount($value, 2, 1, true); break; case 'destination_account_ends': $this->searchAccount($value, 2, 2); break; - case '!destination_account_ends': + case '-destination_account_ends': $this->searchAccount($value, 2, 2, true); break; case 'destination_account_nr_starts': $this->searchAccountNr($value, 2, 1); break; - case '!destination_account_nr_starts': + case '-destination_account_nr_starts': $this->searchAccountNr($value, 2, 1, true); break; case 'destination_account_nr_ends': $this->searchAccountNr($value, 2, 2); break; - case '!destination_account_nr_ends': + case '-destination_account_nr_ends': $this->searchAccountNr($value, 2, 2, true); break; case 'destination_account_nr_is': $this->searchAccountNr($value, 2, 4); break; - case '!destination_account_nr_is': + case '-destination_account_nr_is': $this->searchAccountNr($value, 2, 4, true); break; case 'destination_account_is': $this->searchAccount($value, 2, 4); break; - case '!destination_account_is': + case '-destination_account_is': $this->searchAccount($value, 2, 4, true); break; case 'destination_account_nr_contains': $this->searchAccountNr($value, 2, 3); break; - case '!destination_account_nr_contains': + case '-destination_account_nr_contains': $this->searchAccountNr($value, 2, 3, true); break; case 'destination_account_contains': $this->searchAccount($value, 2, 3); break; - case '!destination_account_contains': + case '-destination_account_contains': $this->searchAccount($value, 2, 3, true); break; case 'destination_account_id': @@ -464,7 +464,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!destination_account_id': + case '-destination_account_id': $account = $this->accountRepository->find((int) $value); if (null !== $account) { $this->collector->excludeDestinationAccounts(new Collection([$account])); @@ -489,7 +489,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!account_id': + case '-account_id': $parts = explode(',', $value); $collection = new Collection; foreach ($parts as $accountId) { @@ -512,7 +512,7 @@ class OperatorQuerySearch implements SearchInterface $account = $this->getCashAccount(); $this->collector->setSourceAccounts(new Collection([$account])); break; - case '!source_is_cash': + case '-source_is_cash': $account = $this->getCashAccount(); $this->collector->excludeSourceAccounts(new Collection([$account])); break; @@ -520,7 +520,7 @@ class OperatorQuerySearch implements SearchInterface $account = $this->getCashAccount(); $this->collector->setDestinationAccounts(new Collection([$account])); break; - case '!destination_is_cash': + case '-destination_is_cash': $account = $this->getCashAccount(); $this->collector->excludeDestinationAccounts(new Collection([$account])); break; @@ -528,7 +528,7 @@ class OperatorQuerySearch implements SearchInterface $account = $this->getCashAccount(); $this->collector->setAccounts(new Collection([$account])); break; - case '!account_is_cash': + case '-account_is_cash': $account = $this->getCashAccount(); $this->collector->excludeAccounts(new Collection([$account])); break; @@ -538,27 +538,27 @@ class OperatorQuerySearch implements SearchInterface case 'description_starts': $this->collector->descriptionStarts([$value]); break; - case '!description_starts': + case '-description_starts': $this->collector->descriptionDoesNotStart([$value]); break; case 'description_ends': $this->collector->descriptionEnds([$value]); break; - case '!description_ends': + case '-description_ends': $this->collector->descriptionDoesNotEnd([$value]); break; case 'description_contains': $this->words[] = $value; return false; - case '!description_contains': + case '-description_contains': $this->prohibitedWords[] = $value; break; case 'description_is': $this->collector->descriptionIs($value); break; - case '!description_is': + case '-description_is': $this->collector->descriptionIsNot($value); break; // @@ -573,7 +573,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!currency_is': + case '-currency_is': $currency = $this->findCurrency($value); if (null !== $currency) { $this->collector->excludeCurrency($currency); @@ -591,7 +591,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!foreign_currency_is': + case '-foreign_currency_is': $currency = $this->findCurrency($value); if (null !== $currency) { $this->collector->excludeForeignCurrency($currency); @@ -604,22 +604,22 @@ class OperatorQuerySearch implements SearchInterface // attachments // case 'has_attachments': - case '!has_no_attachments': + case '-has_no_attachments': Log::debug('Set collector to filter on attachments.'); $this->collector->hasAttachments(); break; case 'has_no_attachments': - case '!has_attachments': + case '-has_attachments': Log::debug('Set collector to filter on NO attachments.'); $this->collector->hasNoAttachments(); break; // // categories - case '!has_any_category': + case '-has_any_category': case 'has_no_category': $this->collector->withoutCategory(); break; - case '!has_no_category': + case '-has_no_category': case 'has_any_category': $this->collector->withCategory(); break; @@ -631,7 +631,7 @@ class OperatorQuerySearch implements SearchInterface } $this->collector->findNothing(); break; - case '!category_is': + case '-category_is': $category = $this->categoryRepository->findByName($value); if (null !== $category) { $this->collector->excludeCategory($category); @@ -647,7 +647,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!category_ends': + case '-category_ends': $result = $this->categoryRepository->categoryEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); @@ -665,7 +665,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!category_starts': + case '-category_starts': $result = $this->categoryRepository->categoryStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); @@ -683,7 +683,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!category_contains': + case '-category_contains': $result = $this->categoryRepository->searchCategory($value, 1337); if ($result->count() > 0) { $this->collector->excludeCategories($result); @@ -695,12 +695,12 @@ class OperatorQuerySearch implements SearchInterface // // budgets // - case '!has_any_budget': + case '-has_any_budget': case 'has_no_budget': $this->collector->withoutBudget(); break; case 'has_any_budget': - case '!has_no_budget': + case '-has_no_budget': $this->collector->withBudget(); break; case 'budget_contains': @@ -712,7 +712,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!budget_contains': + case '-budget_contains': $result = $this->budgetRepository->searchBudget($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); @@ -729,7 +729,7 @@ class OperatorQuerySearch implements SearchInterface } $this->collector->findNothing(); break; - case '!budget_is': + case '-budget_is': $budget = $this->budgetRepository->findByName($value); if (null !== $budget) { $this->collector->excludeBudget($budget); @@ -746,7 +746,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!budget_ends': + case '-budget_ends': $result = $this->budgetRepository->budgetEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); @@ -764,7 +764,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!budget_starts': + case '-budget_starts': $result = $this->budgetRepository->budgetStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBudgets($result); @@ -776,11 +776,11 @@ class OperatorQuerySearch implements SearchInterface // // bill // - case '!has_any_bill': + case '-has_any_bill': case 'has_no_bill': $this->collector->withoutBill(); break; - case '!has_no_bill': + case '-has_no_bill': case 'has_any_bill': $this->collector->withBill(); break; @@ -792,7 +792,7 @@ class OperatorQuerySearch implements SearchInterface } $this->collector->findNothing(); break; - case '!bill_contains': + case '-bill_contains': $result = $this->billRepository->searchBill($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); @@ -808,7 +808,7 @@ class OperatorQuerySearch implements SearchInterface } $this->collector->findNothing(); break; - case '!bill_is': + case '-bill_is': $bill = $this->billRepository->findByName($value); if (null !== $bill) { $this->collector->excludeBills(new Collection([$bill])); @@ -825,7 +825,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!bill_ends': + case '-bill_ends': $result = $this->billRepository->billEndsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); @@ -843,7 +843,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!bill_starts': + case '-bill_starts': $result = $this->billRepository->billStartsWith($value, 1337); if ($result->count() > 0) { $this->collector->excludeBills($result); @@ -855,15 +855,15 @@ class OperatorQuerySearch implements SearchInterface // // tags // - case '!has_any_tag': + case '-has_any_tag': case 'has_no_tag': $this->collector->withoutTags(); break; - case '!has_no_tag': + case '-has_no_tag': case 'has_any_tag': $this->collector->hasAnyTag(); break; - case '!tag_is_not': + case '-tag_is_not': case 'tag_is': $result = $this->tagRepository->searchTag($value); if ($result->count() > 0) { @@ -875,7 +875,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; - case '!tag_is': + case '-tag_is': case 'tag_is_not': $result = $this->tagRepository->searchTag($value); if ($result->count() > 0) { @@ -888,39 +888,39 @@ class OperatorQuerySearch implements SearchInterface case 'notes_contains': $this->collector->notesContain($value); break; - case '!notes_contains': + case '-notes_contains': $this->collector->notesDoNotContain($value); break; case 'notes_starts': $this->collector->notesStartWith($value); break; - case '!notes_starts': + case '-notes_starts': $this->collector->notesDontStartWith($value); break; case 'notes_ends': $this->collector->notesEndWith($value); break; - case '!notes_ends': + case '-notes_ends': $this->collector->notesDontEndWith($value); break; case 'notes_is': $this->collector->notesExactly($value); break; - case '!notes_is': + case '-notes_is': $this->collector->notesExactlyNot($value); break; - case '!any_notes': + case '-any_notes': case 'no_notes': $this->collector->withoutNotes(); break; case 'any_notes': - case '!no_notes': + case '-no_notes': $this->collector->withAnyNotes(); break; case 'reconciled': $this->collector->isReconciled(); break; - case '!reconciled': + case '-reconciled': $this->collector->isNotReconciled(); break; // @@ -934,7 +934,7 @@ class OperatorQuerySearch implements SearchInterface Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountIs($amount); break; - case '!amount_is': + case '-amount_is': // strip comma's, make dots. Log::debug(sprintf('Original value "%s"', $value)); $value = str_replace(',', '.', (string) $value); @@ -951,7 +951,7 @@ class OperatorQuerySearch implements SearchInterface Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountIs($amount); break; - case '!foreign_amount_is': + case '-foreign_amount_is': // strip comma's, make dots. $value = str_replace(',', '.', (string) $value); @@ -960,7 +960,7 @@ class OperatorQuerySearch implements SearchInterface Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountIsNot($amount); break; - case '!amount_more': + case '-amount_more': case 'amount_less': // strip comma's, make dots. $value = str_replace(',', '.', (string) $value); @@ -969,7 +969,7 @@ class OperatorQuerySearch implements SearchInterface Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountLess($amount); break; - case '!foreign_amount_more': + case '-foreign_amount_more': case 'foreign_amount_less': // strip comma's, make dots. $value = str_replace(',', '.', (string) $value); @@ -978,7 +978,7 @@ class OperatorQuerySearch implements SearchInterface Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->foreignAmountLess($amount); break; - case '!amount_less': + case '-amount_less': case 'amount_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. @@ -987,7 +987,7 @@ class OperatorQuerySearch implements SearchInterface Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountMore($amount); break; - case '!foreign_amount_less': + case '-foreign_amount_less': case 'foreign_amount_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. @@ -1003,158 +1003,158 @@ class OperatorQuerySearch implements SearchInterface $this->collector->setTypes([ucfirst($value)]); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); break; - case '!transaction_type': + case '-transaction_type': $this->collector->excludeTypes([ucfirst($value)]); Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); break; // // dates // - case '!date_on': + case '-date_on': case 'date_on': $range = $this->parseDateRange($value); $this->setExactDateParams($range, $prohibited); return false; case 'date_before': - case '!date_after': + case '-date_after': $range = $this->parseDateRange($value); $this->setDateBeforeParams($range); return false; case 'date_after': - case '!date_before': + case '-date_before': $range = $this->parseDateRange($value); $this->setDateAfterParams($range); return false; case 'interest_date_on': - case '!interest_date_on': + case '-interest_date_on': $range = $this->parseDateRange($value); $this->setExactMetaDateParams('interest_date', $range, $prohibited); return false; case 'interest_date_before': - case '!interest_date_after': + case '-interest_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('interest_date', $range); return false; case 'interest_date_after': - case '!interest_date_before': + case '-interest_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('interest_date', $range); return false; case 'book_date_on': - case '!book_date_on': + case '-book_date_on': $range = $this->parseDateRange($value); $this->setExactMetaDateParams('book_date', $range, $prohibited); return false; case 'book_date_before': - case '!book_date_after': + case '-book_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('book_date', $range); return false; case 'book_date_after': - case '!book_date_before': + case '-book_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('book_date', $range); return false; case 'process_date_on': - case '!process_date_on': + case '-process_date_on': $range = $this->parseDateRange($value); $this->setExactMetaDateParams('process_date', $range, $prohibited); return false; case 'process_date_before': - case '!process_date_after': + case '-process_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('process_date', $range); return false; case 'process_date_after': - case '!process_date_before': + case '-process_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('process_date', $range); return false; case 'due_date_on': - case '!due_date_on': + case '-due_date_on': $range = $this->parseDateRange($value); $this->setExactMetaDateParams('due_date', $range, $prohibited); return false; case 'due_date_before': - case '!due_date_after': + case '-due_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('due_date', $range); return false; case 'due_date_after': - case '!due_date_before': + case '-due_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('due_date', $range); return false; case 'payment_date_on': - case '!payment_date_on': + case '-payment_date_on': $range = $this->parseDateRange($value); $this->setExactMetaDateParams('payment_date', $range, $prohibited); return false; case 'payment_date_before': - case '!payment_date_after': + case '-payment_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('payment_date', $range); return false; case 'payment_date_after': - case '!payment_date_before': + case '-payment_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('payment_date', $range); return false; case 'invoice_date_on': - case '!invoice_date_on': + case '-invoice_date_on': $range = $this->parseDateRange($value); $this->setExactMetaDateParams('invoice_date', $range, $prohibited); return false; case 'invoice_date_before': - case '!invoice_date_after': + case '-invoice_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('invoice_date', $range); return false; case 'invoice_date_after': - case '!invoice_date_before': + case '-invoice_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('invoice_date', $range); return false; case 'created_at_on': - case '!created_at_on': + case '-created_at_on': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); $range = $this->parseDateRange($value); $this->setExactObjectDateParams('created_at', $range, $prohibited); return false; case 'created_at_before': - case '!created_at_after': + case '-created_at_after': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); $range = $this->parseDateRange($value); $this->setObjectDateBeforeParams('created_at', $range); return false; case 'created_at_after': - case '!created_at_before': + case '-created_at_before': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); $range = $this->parseDateRange($value); $this->setObjectDateAfterParams('created_at', $range); return false; case 'updated_at_on': - case '!updated_at_on': + case '-updated_at_on': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); $range = $this->parseDateRange($value); $this->setExactObjectDateParams('updated_at', $range, $prohibited); return false; case 'updated_at_before': - case '!updated_at_after': + case '-updated_at_after': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); $range = $this->parseDateRange($value); $this->setObjectDateBeforeParams('updated_at', $range); return false; case 'updated_at_after': - case '!updated_at_before': + case '-updated_at_before': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); $range = $this->parseDateRange($value); $this->setObjectDateAfterParams('updated_at', $range); @@ -1162,87 +1162,154 @@ class OperatorQuerySearch implements SearchInterface // // external URL // - case '!any_external_url': + case '-any_external_url': case 'no_external_url': $this->collector->withoutExternalUrl(); break; - case '!no_external_url': + case '-no_external_url': case 'any_external_url': $this->collector->withExternalUrl(); break; + case 'external_url_is': $this->collector->setExternalUrl($value); break; + case '-external_url_is': + $this->collector->excludeExternalUrl($value); + break; case 'external_url_contains': $this->collector->externalUrlContains($value); break; + case '-external_url_contains': + $this->collector->externalUrlDoesNotContain($value); + break; case 'external_url_starts': $this->collector->externalUrlStarts($value); break; + case '-external_url_starts': + $this->collector->externalUrlDoesNotStart($value); + break; case 'external_url_ends': $this->collector->externalUrlEnds($value); break; + case '-external_url_ends': + $this->collector->externalUrlDoesNotEnd($value); + break; + // // other fields // case 'external_id_is': $this->collector->setExternalId($value); break; + case '-external_id_is': + $this->collector->excludeExternalId($value); + break; case 'recurrence_id': $this->collector->setRecurrenceId($value); break; + case '-recurrence_id': + $this->collector->excludeRecurrenceId($value); + break; case 'external_id_contains': $this->collector->externalIdContains($value); break; + case '-external_id_contains': + $this->collector->externalIdDoesNotContain($value); + break; case 'external_id_starts': $this->collector->externalIdStarts($value); break; + case '-external_id_starts': + $this->collector->externalIdDoesNotStart($value); + break; case 'external_id_ends': $this->collector->externalIdEnds($value); break; + case '-external_id_ends': + $this->collector->externalIdDoesNotEnd($value); + break; case 'internal_reference_is': $this->collector->setInternalReference($value); break; + case '-internal_reference_is': + $this->collector->excludeInternalReference($value); + break; case 'internal_reference_contains': $this->collector->internalReferenceContains($value); break; + case '-internal_reference_contains': + $this->collector->internalReferenceDoesNotContain($value); + break; case 'internal_reference_starts': $this->collector->internalReferenceStarts($value); break; + case '-internal_reference_starts': + $this->collector->internalReferenceDoesNotStart($value); + break; case 'internal_reference_ends': $this->collector->internalReferenceEnds($value); break; + case '-internal_reference_ends': + $this->collector->internalReferenceDoesNotEnd($value); + break; case 'attachment_name_is': $this->collector->attachmentNameIs($value); break; + case '-attachment_name_is': + $this->collector->attachmentNameIsNot($value); + break; case 'attachment_name_contains': $this->collector->attachmentNameContains($value); break; + case '-attachment_name_contains': + $this->collector->attachmentNameDoesNotContain($value); + break; case 'attachment_name_starts': $this->collector->attachmentNameStarts($value); break; + case '-attachment_name_starts': + $this->collector->attachmentNameDoesNotStart($value); + break; case 'attachment_name_ends': $this->collector->attachmentNameEnds($value); break; + case '-attachment_name_ends': + $this->collector->attachmentNameDoesNotEnd($value); + break; case 'attachment_notes_are': $this->collector->attachmentNotesAre($value); break; + case '-attachment_notes_are': + $this->collector->attachmentNotesAreNot($value); + break; case 'attachment_notes_contains': $this->collector->attachmentNotesContains($value); break; + case '-attachment_notes_contains': + $this->collector->attachmentNotesDoNotContain($value); + break; case 'attachment_notes_starts': $this->collector->attachmentNotesStarts($value); break; + case '-attachment_notes_starts': + $this->collector->attachmentNotesDoNotStart($value); + break; case 'attachment_notes_ends': $this->collector->attachmentNotesEnds($value); break; - + case '-attachment_notes_ends': + $this->collector->attachmentNotesDoNotEnd($value); + break; case 'exists': $this->collector->exists(); break; + case '-exists': + $this->collector->findNothing(); + break; } return true; @@ -1257,9 +1324,9 @@ class OperatorQuerySearch implements SearchInterface public static function getRootOperator(string $operator): string { $original = $operator; - // if the string starts with "!" (not), we can remove it and recycle + // if the string starts with "-" (not), we can remove it and recycle // the configuration from the original operator. - if (str_starts_with($operator, '!')) { + if (str_starts_with($operator, '-')) { $operator = substr($operator, 1); } @@ -1269,8 +1336,8 @@ class OperatorQuerySearch implements SearchInterface } if (true === $config['alias']) { $return = $config['alias_for']; - if (str_starts_with($original, '!')) { - $return = sprintf('!%s', $config['alias_for']); + if (str_starts_with($original, '-')) { + $return = sprintf('-%s', $config['alias_for']); } Log::debug(sprintf('"%s" is an alias for "%s", so return that instead.', $original, $return)); @@ -1423,7 +1490,7 @@ class OperatorQuerySearch implements SearchInterface Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count())); $filtered = $accounts->filter( function (Account $account) use ($value, $stringMethod) { - // either IBAN or account number! + // either IBAN or account number $ibanMatch = $stringMethod(strtolower((string) $account->iban), strtolower((string) $value)); $accountNrMatch = false; /** @var AccountMeta $meta */ diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index e1e53b521f..5123bce5ba 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -421,7 +421,8 @@ class General extends AbstractExtension return new TwigFunction( 'getRootSearchOperator', static function (string $operator): string { - return OperatorQuerySearch::getRootOperator($operator); + $result = OperatorQuerySearch::getRootOperator($operator); + return str_replace('-', 'not_', $result); } ); } diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index b2bf5e872a..da65c82e40 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -327,14 +327,17 @@ return [ 'search_modifier_reconciled' => 'Transaction is reconciled', 'search_modifier_not_reconciled' => 'Transaction is not reconciled', 'search_modifier_id' => 'Transaction ID is ":value"', + 'search_modifier_not_id' => 'Transaction ID is not ":value"', 'search_modifier_date_before' => 'Transaction date is before or on ":value"', 'search_modifier_date_after' => 'Transaction date is after or on ":value"', 'search_modifier_external_id_is' => 'External ID is ":value"', + 'search_modifier_not_external_id_is' => 'External ID is not ":value"', 'search_modifier_no_external_url' => 'The transaction has no external URL', 'search_modifier_not_any_external_url' => 'The transaction has no external URL', 'search_modifier_any_external_url' => 'The transaction must have a (any) external URL', 'search_modifier_not_no_external_url' => 'The transaction must have a (any) external URL', 'search_modifier_internal_reference_is' => 'Internal reference is ":value"', + 'search_modifier_not_internal_reference_is' => 'Internal reference is not ":value"', 'search_modifier_description_starts' => 'Description starts with ":value"', 'search_modifier_not_description_starts' => 'Description does not start with ":value"', 'search_modifier_description_ends' => 'Description ends on ":value"', @@ -395,6 +398,7 @@ return [ 'search_modifier_source_account_nr_is' => 'Source account number (IBAN) is ":value"', 'search_modifier_not_source_account_nr_is' => 'Source account number (IBAN) is not ":value"', 'search_modifier_source_account_nr_contains' => 'Source account number (IBAN) contains ":value"', + 'search_modifier_not_source_account_nr_contains' => 'Source account number (IBAN) does not contain ":value"', 'search_modifier_source_account_nr_starts' => 'Source account number (IBAN) starts with ":value"', 'search_modifier_not_source_account_nr_starts' => 'Source account number (IBAN) does not start with ":value"', 'search_modifier_source_account_nr_ends' => 'Source account number (IBAN) ends on ":value"', @@ -414,6 +418,7 @@ return [ 'search_modifier_source_is_cash' => 'Source account is the "(cash)" account', 'search_modifier_not_source_is_cash' => 'Source account is not the "(cash)" account', 'search_modifier_destination_account_nr_is' => 'Destination account number (IBAN) is ":value"', + 'search_modifier_not_destination_account_nr_is' => 'Destination account number (IBAN) is ":value"', 'search_modifier_destination_account_nr_contains' => 'Destination account number (IBAN) contains ":value"', 'search_modifier_not_destination_account_nr_contains' => 'Destination account number (IBAN) does not contain ":value"', 'search_modifier_destination_account_nr_starts' => 'Destination account number (IBAN) starts with ":value"', @@ -429,6 +434,7 @@ return [ 'search_modifier_bill_is' => 'Bill is ":value"', 'search_modifier_not_bill_is' => 'Bill is not ":value"', 'search_modifier_transaction_type' => 'Transaction type is ":value"', + 'search_modifier_not_transaction_type' => 'Transaction type is not ":value"', 'search_modifier_tag_is' => 'Tag is ":value"', 'search_modifier_not_tag_is' => 'No tag is ":value"', 'search_modifier_date_on_year' => 'Transaction is in year ":value"', @@ -469,6 +475,7 @@ return [ 'search_modifier_category_ends' => 'Category ends on ":value"', 'search_modifier_not_category_ends' => 'Category does not end on ":value"', 'search_modifier_category_starts' => 'Category starts with ":value"', + 'search_modifier_not_category_starts' => 'Category does not start with ":value"', 'search_modifier_budget_contains' => 'Budget contains ":value"', 'search_modifier_not_budget_contains' => 'Budget does not contain ":value"', 'search_modifier_budget_ends' => 'Budget ends with ":value"', @@ -482,15 +489,25 @@ return [ 'search_modifier_bill_starts' => 'Bill starts with ":value"', 'search_modifier_not_bill_starts' => 'Bill does not start with ":value"', 'search_modifier_external_id_contains' => 'External ID contains ":value"', + 'search_modifier_not_external_id_contains' => 'External ID does not contain ":value"', 'search_modifier_external_id_ends' => 'External ID ends with ":value"', + 'search_modifier_not_external_id_ends' => 'External ID does not end with ":value"', 'search_modifier_external_id_starts' => 'External ID starts with ":value"', + 'search_modifier_not_external_id_starts' => 'External ID does not start with ":value"', 'search_modifier_internal_reference_contains' => 'Internal reference contains ":value"', + 'search_modifier_not_internal_reference_contains' => 'Internal reference does not contain ":value"', 'search_modifier_internal_reference_ends' => 'Internal reference ends with ":value"', 'search_modifier_internal_reference_starts' => 'Internal reference starts with ":value"', + 'search_modifier_not_internal_reference_ends' => 'Internal reference does not end with ":value"', + 'search_modifier_not_internal_reference_starts' => 'Internal reference does not start with ":value"', 'search_modifier_external_url_is' => 'External URL is ":value"', + 'search_modifier_not_external_url_is' => 'External URL is not ":value"', 'search_modifier_external_url_contains' => 'External URL contains ":value"', + 'search_modifier_not_external_url_contains' => 'External URL does not ":value"', 'search_modifier_external_url_ends' => 'External URL ends with ":value"', + 'search_modifier_not_external_url_ends' => 'External URL does not end with ":value"', 'search_modifier_external_url_starts' => 'External URL starts with ":value"', + 'search_modifier_not_external_url_starts' => 'External URL does not start with ":value"', 'search_modifier_has_no_attachments' => 'Transaction has no attachments', 'search_modifier_not_has_no_attachments' => 'Transaction has attachments', 'search_modifier_not_has_attachments' => 'Transaction has no attachments', @@ -499,12 +516,15 @@ return [ 'search_modifier_journal_id' => 'The journal ID is ":value"', 'search_modifier_not_journal_id' => 'The journal ID is not ":value"', 'search_modifier_recurrence_id' => 'The recurring transaction ID is ":value"', + 'search_modifier_not_recurrence_id' => 'The recurring transaction ID is not ":value"', 'search_modifier_foreign_amount_is' => 'The foreign amount is ":value"', + 'search_modifier_not_foreign_amount_is' => 'The foreign amount is not ":value"', 'search_modifier_foreign_amount_less' => 'The foreign amount is less than ":value"', 'search_modifier_not_foreign_amount_more' => 'The foreign amount is less than ":value"', 'search_modifier_not_foreign_amount_less' => 'The foreign amount is more than ":value"', 'search_modifier_foreign_amount_more' => 'The foreign amount is more than ":value"', 'search_modifier_exists' => 'Transaction exists (any transaction)', + 'search_modifier_not_exists' => 'Transaction does not exist (no transaction)', // date fields 'search_modifier_interest_date_on' => 'Transaction interest date is ":value"', @@ -644,7 +664,15 @@ return [ 'search_modifier_attachment_notes_are' => 'Any attachment\'s notes are ":value"', 'search_modifier_attachment_notes_contains' => 'Any attachment\'s notes contain ":value"', 'search_modifier_attachment_notes_starts' => 'Any attachment\'s notes start with ":value"', - 'search_modifier_attachment_notes_ends' => 'Any attachment\'s notes end is ":value"', + 'search_modifier_attachment_notes_ends' => 'Any attachment\'s notes end with ":value"', + 'search_modifier_not_attachment_name_is' => 'Any attachment\'s name is not ":value"', + 'search_modifier_not_attachment_name_contains' => 'Any attachment\'s name does not contain ":value"', + 'search_modifier_not_attachment_name_starts' => 'Any attachment\'s name does not start with ":value"', + 'search_modifier_not_attachment_name_ends' => 'Any attachment\'s name does not end with ":value"', + 'search_modifier_not_attachment_notes_are' => 'Any attachment\'s notes are not ":value"', + 'search_modifier_not_attachment_notes_contains' => 'Any attachment\'s notes do not contain ":value"', + 'search_modifier_not_attachment_notes_starts' => 'Any attachment\'s notes start with ":value"', + 'search_modifier_not_attachment_notes_ends' => 'Any attachment\'s notes do not end with ":value"', 'update_rule_from_query' => 'Update rule ":rule" from search query', 'create_rule_from_query' => 'Create new rule from search query', 'rule_from_search_words' => 'The rule engine has a hard time handling ":string". The suggested rule that fits your search query may give different results. Please verify the rule triggers carefully.', @@ -1001,6 +1029,136 @@ return [ 'rule_trigger_exists_choice' => 'Any transaction matches(!)', 'rule_trigger_exists' => 'Any transaction matches', + // more values for new types: + 'rule_trigger_not_account_id' => 'Account ID is not ":trigger_value"', + 'rule_trigger_not_source_account_id' => 'Source account ID is not ":trigger_value"', + 'rule_trigger_not_destination_account_id' => 'Destination account ID is not ":trigger_value"', + 'rule_trigger_not_transaction_type' => 'Transaction type is not ":trigger_value"', + 'rule_trigger_not_tag_is' => 'Tag is not ":trigger_value"', + 'rule_trigger_not_tag_is_not' => 'Tag is ":trigger_value"', + 'rule_trigger_not_description_is' => 'Description is not ":trigger_value"', + 'rule_trigger_not_description_contains' => 'Description does not contain', + 'rule_trigger_not_description_ends' => 'Description does not end with ":trigger_value"', + 'rule_trigger_not_description_starts' => 'Description does not start with ":trigger_value"', + 'rule_trigger_not_notes_is' => 'Notes are not ":trigger_value"', + 'rule_trigger_not_notes_contains' => 'Notes do not contain ":trigger_value"', + 'rule_trigger_not_notes_ends' => 'Notes do not end on ":trigger_value"', + 'rule_trigger_not_notes_starts' => 'Notes do not start with ":trigger_value"', + 'rule_trigger_not_source_account_is' => 'Source account is not ":trigger_value"', + 'rule_trigger_not_source_account_contains' => 'Source account does not contain ":trigger_value"', + 'rule_trigger_not_source_account_ends' => 'Source account does not end on ":trigger_value"', + 'rule_trigger_not_source_account_starts' => 'Source account does not start with ":trigger_value"', + 'rule_trigger_not_source_account_nr_is' => 'Source account number / IBAN is not ":trigger_value"', + 'rule_trigger_not_source_account_nr_contains' => 'Source account number / IBAN does not contain ":trigger_value"', + 'rule_trigger_not_source_account_nr_ends' => 'Source account number / IBAN does not end on ":trigger_value"', + 'rule_trigger_not_source_account_nr_starts' => 'Source account number / IBAN does not start with ":trigger_value"', + 'rule_trigger_not_destination_account_is' => 'Destination account is not ":trigger_value"', + 'rule_trigger_not_destination_account_contains' => 'Destination account does not contain ":trigger_value"', + 'rule_trigger_not_destination_account_ends' => 'Destination account does not end on ":trigger_value"', + 'rule_trigger_not_destination_account_starts' => 'Destination account does not start with ":trigger_value"', + 'rule_trigger_not_destination_account_nr_is' => 'Destination account number / IBAN is not ":trigger_value"', + 'rule_trigger_not_destination_account_nr_contains' => 'Destination account number / IBAN does not contain ":trigger_value"', + 'rule_trigger_not_destination_account_nr_ends' => 'Destination account number / IBAN does not end on ":trigger_value"', + 'rule_trigger_not_destination_account_nr_starts' => 'Destination account number / IBAN does not start with ":trigger_value"', + 'rule_trigger_not_account_is' => 'Neither account is ":trigger_value"', + 'rule_trigger_not_account_contains' => 'Neither account contains ":trigger_value"', + 'rule_trigger_not_account_ends' => 'Neither account ends on ":trigger_value"', + 'rule_trigger_not_account_starts' => 'Neither account starts with ":trigger_value"', + 'rule_trigger_not_account_nr_is' => 'Neither account number / IBAN is ":trigger_value"', + 'rule_trigger_not_account_nr_contains' => 'Neither account number / IBAN contains ":trigger_value"', + 'rule_trigger_not_account_nr_ends' => 'Neither account number / IBAN ends on ":trigger_value"', + 'rule_trigger_not_account_nr_starts' => 'Neither account number / IBAN starts with ":trigger_value"', + 'rule_trigger_not_category_is' => 'Neither category is ":trigger_value"', + 'rule_trigger_not_category_contains' => 'Neither category contains ":trigger_value"', + 'rule_trigger_not_category_ends' => 'Neither category ends on ":trigger_value"', + 'rule_trigger_not_category_starts' => 'Neither category starts with ":trigger_value"', + 'rule_trigger_not_budget_is' => 'Neither budget is ":trigger_value"', + 'rule_trigger_not_budget_contains' => 'Neither budget contains ":trigger_value"', + 'rule_trigger_not_budget_ends' => 'Neither budget ends on ":trigger_value"', + 'rule_trigger_not_budget_starts' => 'Neither budget starts with ":trigger_value"', + 'rule_trigger_not_bill_is' => 'Neither bill is ":trigger_value"', + 'rule_trigger_not_bill_contains' => 'Neither bill contains ":trigger_value"', + 'rule_trigger_not_bill_ends' => 'Neither bill ends on ":trigger_value"', + 'rule_trigger_not_bill_starts' => 'Neither bill starts with ":trigger_value"', + 'rule_trigger_not_external_id_is' => 'External ID is not ":trigger_value"', + 'rule_trigger_not_external_id_contains' => 'External ID does not contain ":trigger_value"', + 'rule_trigger_not_external_id_ends' => 'External ID does not end on ":trigger_value"', + 'rule_trigger_not_external_id_starts' => 'External ID does not start with ":trigger_value"', + 'rule_trigger_not_internal_reference_is' => 'Internal reference is not ":trigger_value"', + 'rule_trigger_not_internal_reference_contains' => 'Internal reference does not contain ":trigger_value"', + 'rule_trigger_not_internal_reference_ends' => 'Internal reference does not end on ":trigger_value"', + 'rule_trigger_not_internal_reference_starts' => 'Internal reference does not start with ":trigger_value"', + 'rule_trigger_not_external_url_is' => 'External URL is not ":trigger_value"', + 'rule_trigger_not_external_url_contains' => 'External URL does not contain ":trigger_value"', + 'rule_trigger_not_external_url_ends' => 'External URL does not end on ":trigger_value"', + 'rule_trigger_not_external_url_starts' => 'External URL does not start with ":trigger_value"', + 'rule_trigger_not_currency_is' => 'Currency is not ":trigger_value"', + 'rule_trigger_not_foreign_currency_is' => 'Foreign currency is not ":trigger_value"', + 'rule_trigger_not_id' => 'Transaction ID is not ":trigger_value"', + 'rule_trigger_not_journal_id' => 'Transaction journal ID is not ":trigger_value"', + 'rule_trigger_not_recurrence_id' => 'Recurrence ID is not ":trigger_value"', + 'rule_trigger_not_date_on' => 'Date is not on ":trigger_value"', + 'rule_trigger_not_date_before' => 'Date is not before ":trigger_value"', + 'rule_trigger_not_date_after' => 'Date is not after ":trigger_value"', + 'rule_trigger_not_interest_date_on' => 'Interest date is not on ":trigger_value"', + 'rule_trigger_not_interest_date_before' => 'Interest date is not before ":trigger_value"', + 'rule_trigger_not_interest_date_after' => 'Interest date is not after ":trigger_value"', + 'rule_trigger_not_book_date_on' => 'Book date is not on ":trigger_value"', + 'rule_trigger_not_book_date_before' => 'Book date is not before ":trigger_value"', + 'rule_trigger_not_book_date_after' => 'Book date is not after ":trigger_value"', + 'rule_trigger_not_process_date_on' => 'Process date is not on ":trigger_value"', + 'rule_trigger_not_process_date_before' => 'Process date is not before ":trigger_value"', + 'rule_trigger_not_process_date_after' => 'Process date is not after ":trigger_value"', + 'rule_trigger_not_due_date_on' => 'Due date is not on ":trigger_value"', + 'rule_trigger_not_due_date_before' => 'Due date is not before ":trigger_value"', + 'rule_trigger_not_due_date_after' => 'Due date is not after ":trigger_value"', + 'rule_trigger_not_payment_date_on' => 'Payment date is not on ":trigger_value"', + 'rule_trigger_not_payment_date_before' => 'Payment date is not before ":trigger_value"', + 'rule_trigger_not_payment_date_after' => 'Payment date is not after ":trigger_value"', + 'rule_trigger_not_invoice_date_on' => 'Invoice date is not on ":trigger_value"', + 'rule_trigger_not_invoice_date_before' => 'Invoice date is not before ":trigger_value"', + 'rule_trigger_not_invoice_date_after' => 'Invoice date is not after ":trigger_value"', + 'rule_trigger_not_created_at_on' => 'Transaction is not created on ":trigger_value"', + 'rule_trigger_not_created_at_before' => 'Transaction is not created before ":trigger_value"', + 'rule_trigger_not_created_at_after' => 'Transaction is not created after ":trigger_value"', + 'rule_trigger_not_updated_at_on' => 'Transaction is not updated on ":trigger_value"', + 'rule_trigger_not_updated_at_before' => 'Transaction is not updated before ":trigger_value"', + 'rule_trigger_not_updated_at_after' => 'Transaction is not updated after ":trigger_value"', + 'rule_trigger_not_amount_is' => 'Transaction amount is not ":trigger_value"', + 'rule_trigger_not_amount_less' => 'Transaction amount is more than ":trigger_value"', + 'rule_trigger_not_amount_more' => 'Transaction amount is less than ":trigger_value"', + 'rule_trigger_not_foreign_amount_is' => 'Foreign transaction amount is not ":trigger_value"', + 'rule_trigger_not_foreign_amount_less' => 'Foreign transaction amount is more than ":trigger_value"', + 'rule_trigger_not_foreign_amount_more' => 'Foreign transaction amount is less than ":trigger_value"', + 'rule_trigger_not_attachment_name_is' => 'No attachment is named ":trigger_value"', + 'rule_trigger_not_attachment_name_contains' => 'No attachment name contains ":trigger_value"', + 'rule_trigger_not_attachment_name_starts' => 'No attachment name starts with ":trigger_value"', + 'rule_trigger_not_attachment_name_ends' => 'No attachment name ends on ":trigger_value"', + 'rule_trigger_not_attachment_notes_are' => 'No attachment notes are ":trigger_value"', + 'rule_trigger_not_attachment_notes_contains' => 'No attachment notes contain ":trigger_value"', + 'rule_trigger_not_attachment_notes_starts' => 'No attachment notes start with ":trigger_value"', + 'rule_trigger_not_attachment_notes_ends' => 'No attachment notes end on ":trigger_value"', + 'rule_trigger_not_reconciled' => 'Transaction is not reconciled', + 'rule_trigger_not_exists' => 'Transaction does not exist', + 'rule_trigger_not_has_attachments' => 'Transaction has no attachments', + 'rule_trigger_not_has_any_category' => 'Transaction has no category', + 'rule_trigger_not_has_any_budget' => 'Transaction has no category', + 'rule_trigger_not_has_any_bill' => 'Transaction has no bill', + 'rule_trigger_not_has_any_tag' => 'Transaction has no tags', + 'rule_trigger_not_any_notes' => 'Transaction has no notes', + 'rule_trigger_not_any_external_url' => 'Transaction has no external URL', + 'rule_trigger_not_has_no_attachments' => 'Transaction has a (any) attachment(s)', + 'rule_trigger_not_has_no_category' => 'Transaction has a (any) category', + 'rule_trigger_not_has_no_budget' => 'Transaction has a (any) budget', + 'rule_trigger_not_has_no_bill' => 'Transaction has a (any) bill', + 'rule_trigger_not_has_no_tag' => 'Transaction has a (any) tag', + 'rule_trigger_not_no_notes' => 'Transaction has any notes', + 'rule_trigger_not_no_external_url' => 'Transaction has an external URL', + 'rule_trigger_not_source_is_cash' => 'Source account is a not a cash account', + 'rule_trigger_not_destination_is_cash' => 'Destination account is a not a cash account', + 'rule_trigger_not_account_is_cash' => 'Neither account is a cash account', + + // actions 'rule_action_delete_transaction_choice' => 'DELETE transaction(!)', 'rule_action_delete_transaction' => 'DELETE transaction(!)',