diff --git a/app/Helpers/Collector/Extensions/AccountCollection.php b/app/Helpers/Collector/Extensions/AccountCollection.php index 41f3aa35b7..eb335150e2 100644 --- a/app/Helpers/Collector/Extensions/AccountCollection.php +++ b/app/Helpers/Collector/Extensions/AccountCollection.php @@ -72,6 +72,26 @@ trait AccountCollection return $this; } + /** + * These accounts must not be included. + * + * @param Collection $accounts + * + * @return GroupCollectorInterface + */ + public function excludeAccounts(Collection $accounts): GroupCollectorInterface + { + if ($accounts->count() > 0) { + $accountIds = $accounts->pluck('id')->toArray(); + $this->query->whereNotIn('source.account_id', $accountIds); + $this->query->whereNotIn('destination.account_id', $accountIds); + + app('log')->debug(sprintf('GroupCollector: excludeAccounts: %s', implode(', ', $accountIds))); + } + + return $this; + } + /** * Define which accounts can be part of the source and destination transactions. * @@ -95,6 +115,29 @@ trait AccountCollection return $this; } + /** + * Define which accounts can NOT be part of the source and destination transactions. + * + * @param Collection $accounts + * + * @return GroupCollectorInterface + */ + public function setNotAccounts(Collection $accounts): GroupCollectorInterface + { + if ($accounts->count() > 0) { + $accountIds = $accounts->pluck('id')->toArray(); + $this->query->where( + static function (EloquentBuilder $query) use ($accountIds) { + $query->whereNotIn('source.account_id', $accountIds); + $query->whereNotIn('destination.account_id', $accountIds); + } + ); + //app('log')->debug(sprintf('GroupCollector: setAccounts: %s', implode(', ', $accountIds))); + } + + return $this; + } + /** * Both source AND destination must be in this list of accounts. * diff --git a/app/Helpers/Collector/Extensions/AmountCollection.php b/app/Helpers/Collector/Extensions/AmountCollection.php index 99fe7d1742..93ce6cee8b 100644 --- a/app/Helpers/Collector/Extensions/AmountCollection.php +++ b/app/Helpers/Collector/Extensions/AmountCollection.php @@ -51,6 +51,20 @@ trait AmountCollection return $this; } + /** + * @inheritDoc + */ + public function amountIsNot(string $amount): GroupCollectorInterface + { + $this->query->where( + static function (EloquentBuilder $q) use ($amount) { + $q->where('source.amount','!=', app('steam')->negative($amount)); + } + ); + + return $this; + } + /** * Get transactions where the amount is less than. * @@ -106,6 +120,25 @@ trait AmountCollection return $this; } + /** + * Get transactions with a specific foreign amount. + * + * @param string $amount + * + * @return GroupCollectorInterface + */ + public function foreignAmountIsNot(string $amount): GroupCollectorInterface + { + $this->query->where( + static function (EloquentBuilder $q) use ($amount) { + $q->whereNull('source.foreign_amount'); + $q->orWhere('source.foreign_amount','!=', app('steam')->negative($amount)); + } + ); + + return $this; + } + /** * Get transactions where the amount is less than. * diff --git a/app/Helpers/Collector/Extensions/MetaCollection.php b/app/Helpers/Collector/Extensions/MetaCollection.php index 61cc757453..835b981460 100644 --- a/app/Helpers/Collector/Extensions/MetaCollection.php +++ b/app/Helpers/Collector/Extensions/MetaCollection.php @@ -30,6 +30,7 @@ use FireflyIII\Models\Budget; use FireflyIII\Models\Category; use FireflyIII\Models\Tag; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; @@ -226,6 +227,53 @@ trait MetaCollection return $this; } + /** + * @param string $value + * + * @return GroupCollectorInterface + */ + public function notesDoNotContain(string $value): GroupCollectorInterface + { + $this->withNotes(); + $this->query->where(static function (Builder $q) use ($value) { + $q->whereNull('notes.text'); + $q->orWhere('notes.text', 'NOT LIKE', sprintf('%%%s%%', $value)); + }); + + return $this; + } + + /** + * @param string $value + * + * @return GroupCollectorInterface + */ + public function notesDontStartWith(string $value): GroupCollectorInterface + { + $this->withNotes(); + $this->query->where(static function (Builder $q) use ($value) { + $q->whereNull('notes.text'); + $q->orWhere('notes.text', 'NOT LIKE', sprintf('%s%%', $value)); + }); + + return $this; + } + + /** + * @param string $value + * + * @return GroupCollectorInterface + */ + public function notesDontEndWith(string $value): GroupCollectorInterface + { + $this->withNotes(); + $this->query->where(static function (Builder $q) use ($value) { + $q->whereNull('notes.text'); + $q->orWhere('notes.text', 'NOT LIKE', sprintf('%%%s', $value)); + }); + + return $this; + } /** * @inheritDoc @@ -276,6 +324,22 @@ trait MetaCollection return $this; } + /** + * @param string $value + * + * @return GroupCollectorInterface + */ + public function notesExactlyNot(string $value): GroupCollectorInterface + { + $this->withNotes(); + $this->query->where(static function (Builder $q) use ($value) { + $q->whereNull('notes.text'); + $q->orWhere('notes.text', '!=', sprintf('%s', $value)); + }); + + return $this; + } + /** * @param string $value * @@ -338,6 +402,20 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function excludeBills(Collection $bills): GroupCollectorInterface + { + $this->withBillInformation(); + $this->query->where(static function(EloquentBuilder $q1) use ($bills) { + $q1->whereNotIn('transaction_journals.bill_id', $bills->pluck('id')->toArray()); + $q1->orWhereNull('transaction_journals.bill_id'); + }); + + return $this; + } + /** * Limit the search to a specific budget. * @@ -391,6 +469,22 @@ trait MetaCollection return $this; } + /** + * @inheritDoc + */ + public function excludeBudgets(Collection $budgets): GroupCollectorInterface + { + if ($budgets->count() > 0) { + $this->withBudgetInformation(); + $this->query->where(static function (EloquentBuilder $q1) use ($budgets) { + $q1->whereNotIn('budgets.id', $budgets->pluck('id')->toArray()); + $q1->orWhereNull('budgets.id'); + }); + } + + return $this; + } + /** * Limit the search to a specific bunch of categories. * @@ -409,17 +503,16 @@ trait MetaCollection } /** - * Limit the search to a specific bunch of categories. - * - * @param Collection $categories - * - * @return GroupCollectorInterface + * @inheritDoc */ - public function setNotCategories(Collection $categories): GroupCollectorInterface + public function excludeCategories(Collection $categories): GroupCollectorInterface { if ($categories->count() > 0) { $this->withCategoryInformation(); - $this->query->whereNotIn('categories.id', $categories->pluck('id')->toArray()); + $this->query->where(static function (EloquentBuilder $q1) use ($categories) { + $q1->whereNotIn('categories.id', $categories->pluck('id')->toArray()); + $q1->orWhereNull('categories.id'); + }); } return $this; @@ -461,6 +554,44 @@ trait MetaCollection return $this; } + /** + * Exclude a specific category. + * + * @param Category $category + * + * @return GroupCollectorInterface + */ + public function excludeCategory(Category $category): GroupCollectorInterface + { + $this->withCategoryInformation(); + + $this->query->where(static function(EloquentBuilder $q2) use ($category) { + $q2->where('categories.id','!=', $category->id); + $q2->orWhereNull('categories.id'); + }); + + return $this; + } + + /** + * Exclude a specific budget. + * + * @param Budget $budget + * + * @return GroupCollectorInterface + */ + public function excludeBudget(Budget $budget): GroupCollectorInterface + { + $this->withBudgetInformation(); + + $this->query->where(static function(EloquentBuilder $q2) use ($budget) { + $q2->where('budgets.id','!=', $budget->id); + $q2->orWhereNull('budgets.id'); + }); + + return $this; + } + /** * @inheritDoc */ diff --git a/app/Helpers/Collector/Extensions/TimeCollection.php b/app/Helpers/Collector/Extensions/TimeCollection.php index cca0a7855d..8fe5b6bf74 100644 --- a/app/Helpers/Collector/Extensions/TimeCollection.php +++ b/app/Helpers/Collector/Extensions/TimeCollection.php @@ -62,6 +62,16 @@ trait TimeCollection return $this; } + /** + * @param string $day + * @return GroupCollectorInterface + */ + public function dayIsNot(string $day): GroupCollectorInterface + { + $this->query->whereDay('transaction_journals.date', '!=', $day); + return $this; + } + /** * @param string $day * @param string $field @@ -142,6 +152,28 @@ trait TimeCollection return $this; } + /** + * @param string $day + * @param string $field + * @return GroupCollectorInterface + */ + public function metaDayIsNot(string $day, string $field): GroupCollectorInterface + { + $this->withMetaDate($field); + $filter = function (int $index, array $object) use ($field, $day): bool { + foreach ($object['transactions'] as $transaction) { + if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon + ) { + return (int) $day !== $transaction[$field]->day; + } + } + + return false; + }; + $this->postFilters[] = $filter; + return $this; + } + /** * @param string $month * @param string $field @@ -210,6 +242,28 @@ trait TimeCollection return $this; } + /** + * @param string $month + * @param string $field + * @return GroupCollectorInterface + */ + public function metaMonthIsNot(string $month, string $field): GroupCollectorInterface + { + $this->withMetaDate($field); + $filter = function (int $index, array $object) use ($field, $month): bool { + foreach ($object['transactions'] as $transaction) { + if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon + ) { + return (int) $month !== $transaction[$field]->month; + } + } + + return false; + }; + $this->postFilters[] = $filter; + return $this; + } + /** * @param string $year * @param string $field @@ -279,6 +333,28 @@ trait TimeCollection return $this; } + /** + * @param string $year + * @param string $field + * @return GroupCollectorInterface + */ + public function metaYearIsNot(string $year, string $field): GroupCollectorInterface + { + $this->withMetaDate($field); + $filter = function (int $index, array $object) use ($field, $year): bool { + foreach ($object['transactions'] as $transaction) { + if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon + ) { + return $year !== (string) $transaction[$field]->year; + } + } + return true; + }; + $this->postFilters[] = $filter; + + return $this; + } + /** * @param string $month * @return GroupCollectorInterface @@ -311,6 +387,16 @@ trait TimeCollection return $this; } + /** + * @param string $month + * @return GroupCollectorInterface + */ + public function monthIsNot(string $month): GroupCollectorInterface + { + $this->query->whereMonth('transaction_journals.date', '!=', $month); + return $this; + } + /** * @param string $day * @param string $field @@ -344,6 +430,17 @@ trait TimeCollection return $this; } + /** + * @param string $day + * @param string $field + * @return GroupCollectorInterface + */ + public function objectDayIsNot(string $day, string $field): GroupCollectorInterface + { + $this->query->whereDay(sprintf('transaction_journals.%s', $field), '!=', $day); + return $this; + } + /** * @param string $month * @param string $field @@ -377,6 +474,17 @@ trait TimeCollection return $this; } + /** + * @param string $month + * @param string $field + * @return GroupCollectorInterface + */ + public function objectMonthIsNot(string $month, string $field): GroupCollectorInterface + { + $this->query->whereMonth(sprintf('transaction_journals.%s', $field), '!=', $month); + return $this; + } + /** * @param string $year * @param string $field @@ -410,6 +518,17 @@ trait TimeCollection return $this; } + /** + * @param string $year + * @param string $field + * @return GroupCollectorInterface + */ + public function objectYearIsNot(string $year, string $field): GroupCollectorInterface + { + $this->query->whereYear(sprintf('transaction_journals.%s', $field), '!=', $year); + return $this; + } + /** * Collect transactions after a specific date. * @@ -535,6 +654,37 @@ trait TimeCollection } + /** + * @param Carbon $start + * @param Carbon $end + * @param string $field + * @return GroupCollectorInterface + */ + public function excludeMetaDateRange(Carbon $start, Carbon $end, string $field): GroupCollectorInterface + { + if ($end < $start) { + [$start, $end] = [$end, $start]; + } + $end = clone $end; // this is so weird, but it works if $end and $start secretly point to the same object. + $end->endOfDay(); + $start->startOfDay(); + $this->withMetaDate($field); + + $filter = function (int $index, array $object) use ($field, $start, $end): bool { + foreach ($object['transactions'] as $transaction) { + if (array_key_exists($field, $transaction) && $transaction[$field] instanceof Carbon) { + return $transaction[$field]->lt($start) || $transaction[$field]->gt($end); + } + } + + return false; + }; + $this->postFilters[] = $filter; + + return $this; + + } + /** * @param Carbon $date * @param string $field @@ -576,6 +726,23 @@ trait TimeCollection return $this; } + /** + * @param Carbon $start + * @param Carbon $end + * @param string $field + * @return GroupCollectorInterface + */ + public function excludeObjectRange(Carbon $start, Carbon $end, string $field): GroupCollectorInterface + { + $after = $start->format('Y-m-d 00:00:00'); + $before = $end->format('Y-m-d 23:59:59'); + + $this->query->where(sprintf('transaction_journals.%s', $field), '<', $after); + $this->query->orWhere(sprintf('transaction_journals.%s', $field), '>', $before); + + return $this; + } + /** * Set the start and end time of the results to return. * @@ -599,6 +766,27 @@ trait TimeCollection return $this; } + /** + * @param Carbon $start + * @param Carbon $end + * @return GroupCollectorInterface + */ + public function excludeRange(Carbon $start, Carbon $end): GroupCollectorInterface + { + if ($end < $start) { + [$start, $end] = [$end, $start]; + } + $startStr = $start->format('Y-m-d 00:00:00'); + $endStr = $end->format('Y-m-d 23:59:59'); + + $this->query->where('transaction_journals.date', '<', $startStr); + $this->query->orWhere('transaction_journals.date', '>', $endStr); + + return $this; + } + + + /** * Collect transactions updated on a specific date. * @@ -616,22 +804,44 @@ trait TimeCollection return $this; } + /** + * @param string $year + * @return GroupCollectorInterface + */ public function yearAfter(string $year): GroupCollectorInterface { $this->query->whereYear('transaction_journals.date', '>=', $year); return $this; } + /** + * @param string $year + * @return GroupCollectorInterface + */ public function yearBefore(string $year): GroupCollectorInterface { $this->query->whereYear('transaction_journals.date', '<=', $year); return $this; } + /** + * @param string $year + * @return GroupCollectorInterface + */ public function yearIs(string $year): GroupCollectorInterface { $this->query->whereYear('transaction_journals.date', '=', $year); return $this; } + /** + * @param string $year + * @return GroupCollectorInterface + */ + public function yearIsNot(string $year): GroupCollectorInterface + { + $this->query->whereYear('transaction_journals.date', '!=', $year); + return $this; + } + } diff --git a/app/Helpers/Collector/GroupCollector.php b/app/Helpers/Collector/GroupCollector.php index 667204ad36..c13886697e 100644 --- a/app/Helpers/Collector/GroupCollector.php +++ b/app/Helpers/Collector/GroupCollector.php @@ -162,6 +162,36 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * @inheritDoc + */ + public function descriptionDoesNotEnd(array $array): GroupCollectorInterface + { + $this->query->where( + static function (EloquentBuilder $q) use ($array) { + $q->where( + static function (EloquentBuilder $q1) use ($array) { + foreach ($array as $word) { + $keyword = sprintf('%%%s', $word); + $q1->where('transaction_journals.description', 'NOT LIKE', $keyword); + } + } + ); + $q->where( + static function (EloquentBuilder $q2) use ($array) { + foreach ($array as $word) { + $keyword = sprintf('%%%s', $word); + $q2->where('transaction_groups.title', 'NOT LIKE', $keyword); + $q2->orWhereNull('transaction_groups.title'); + } + } + ); + } + ); + + return $this; + } + /** * @inheritDoc */ @@ -177,6 +207,26 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * @inheritDoc + */ + public function descriptionIsNot(string $value): GroupCollectorInterface + { + $this->query->where( + static function (EloquentBuilder $q) use ($value) { + $q->where('transaction_journals.description', '!=', $value); + $q->where( + static function (EloquentBuilder $q2) use ($value) { + $q2->where('transaction_groups.title', '!=', $value); + $q2->orWhereNull('transaction_groups.title'); + } + ); + } + ); + + return $this; + } + /** * @inheritDoc */ @@ -206,6 +256,36 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * @inheritDoc + */ + public function descriptionDoesNotStart(array $array): GroupCollectorInterface + { + $this->query->where( + static function (EloquentBuilder $q) use ($array) { + $q->where( + static function (EloquentBuilder $q1) use ($array) { + foreach ($array as $word) { + $keyword = sprintf('%s%%', $word); + $q1->where('transaction_journals.description', 'NOT LIKE', $keyword); + } + } + ); + $q->where( + static function (EloquentBuilder $q2) use ($array) { + foreach ($array as $word) { + $keyword = sprintf('%s%%', $word); + $q2->where('transaction_groups.title', 'NOT LIKE', $keyword); + $q2->orWhereNull('transaction_groups.title'); + } + } + ); + } + ); + + return $this; + } + /** * */ @@ -546,7 +626,6 @@ class GroupCollector implements GroupCollectorInterface * @var Closure $function */ foreach ($this->postFilters as $function) { - $nextCollection = new Collection; // loop everything in the current collection // and save it (or not) in the new collection. @@ -618,6 +697,30 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * Limit results to NOT a specific currency, either foreign or normal one. + * + * @param TransactionCurrency $currency + * + * @return GroupCollectorInterface + */ + public function excludeCurrency(TransactionCurrency $currency): GroupCollectorInterface + { + $this->query->where( + static function (EloquentBuilder $q) use ($currency) { + $q->where('source.transaction_currency_id','!=', $currency->id); + $q->where( + static function (EloquentBuilder $q2) use ($currency) { + $q2->where('source.foreign_currency_id','!=', $currency->id); + $q2->orWhereNull('source.foreign_currency_id'); + } + ); + } + ); + + return $this; + } + /** * @inheritDoc */ @@ -628,6 +731,19 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * @inheritDoc + */ + public function excludeForeignCurrency(TransactionCurrency $currency): GroupCollectorInterface + { + $this->query->where(static function(EloquentBuilder $q2) use ($currency) { + $q2->where('source.foreign_currency_id','!=', $currency->id); + $q2->orWhereNull('source.foreign_currency_id'); + }); + + return $this; + } + /** * Limit the result to a set of specific transaction groups. * @@ -643,6 +759,21 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * Limit the result to NOT a set of specific transaction groups. + * + * @param array $groupIds + * + * @return GroupCollectorInterface + */ + public function excludeIds(array $groupIds): GroupCollectorInterface + { + + $this->query->whereNotIn('transaction_groups.id', $groupIds); + + return $this; + } + /** * Limit the result to a set of specific journals. * @@ -663,6 +794,26 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * Limit the result to NOT a set of specific journals. + * + * @param array $journalIds + * + * @return GroupCollectorInterface + */ + public function excludeJournalIds(array $journalIds): GroupCollectorInterface + { + if (!empty($journalIds)) { + // make all integers. + $integerIDs = array_map('intval', $journalIds); + + + $this->query->whereNotIn('transaction_journals.id', $integerIDs); + } + + return $this; + } + /** * Set the page to get. * @@ -688,6 +839,9 @@ class GroupCollector implements GroupCollectorInterface */ public function setSearchWords(array $array): GroupCollectorInterface { + if (0 === count($array)) { + return $this; + } $this->query->where( static function (EloquentBuilder $q) use ($array) { $q->where( @@ -712,6 +866,43 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * Search for words in descriptions. + * + * @param array $array + * + * @return GroupCollectorInterface + */ + public function excludeSearchWords(array $array): GroupCollectorInterface + { + if (0 === count($array)) { + return $this; + } + $this->query->where( + static function (EloquentBuilder $q) use ($array) { + $q->where( + static function (EloquentBuilder $q1) use ($array) { + foreach ($array as $word) { + $keyword = sprintf('%%%s%%', $word); + $q1->where('transaction_journals.description', 'NOT LIKE', $keyword); + } + } + ); + $q->where( + static function (EloquentBuilder $q2) use ($array) { + foreach ($array as $word) { + $keyword = sprintf('%%%s%%', $word); + $q2->where('transaction_groups.title', 'NOT LIKE', $keyword); + $q2->orWhereNull('transaction_groups.title'); + } + } + ); + } + ); + + return $this; + } + /** * Limit the search to one specific transaction group. * @@ -740,6 +931,16 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * @inheritDoc + */ + public function excludeTypes(array $types): GroupCollectorInterface + { + $this->query->whereNotIn('transaction_types.type', $types); + + return $this; + } + /** * Set the user object and start the query. * @@ -842,6 +1043,15 @@ class GroupCollector implements GroupCollectorInterface return $this; } + /** + * @inheritDoc + */ + public function isNotReconciled(): GroupCollectorInterface + { + $this->query->where('source.reconciled', 0)->where('destination.reconciled', 0); + return $this; + } + /** * @inheritDoc */ diff --git a/app/Helpers/Collector/GroupCollectorInterface.php b/app/Helpers/Collector/GroupCollectorInterface.php index 3fcec55ce7..ca11ebb896 100644 --- a/app/Helpers/Collector/GroupCollectorInterface.php +++ b/app/Helpers/Collector/GroupCollectorInterface.php @@ -48,6 +48,12 @@ interface GroupCollectorInterface */ public function amountIs(string $amount): GroupCollectorInterface; + /** + * @param string $amount + * + * @return GroupCollectorInterface + */ + public function amountIsNot(string $amount): GroupCollectorInterface; /** * Only journals that are reconciled. @@ -56,6 +62,13 @@ interface GroupCollectorInterface */ public function isReconciled(): GroupCollectorInterface; + /** + * Only journals that are reconciled. + * + * @return GroupCollectorInterface + */ + public function isNotReconciled(): GroupCollectorInterface; + /** * @return GroupCollectorInterface */ @@ -145,6 +158,12 @@ interface GroupCollectorInterface */ public function dayIs(string $day): GroupCollectorInterface; + /** + * @param string $day + * @return GroupCollectorInterface + */ + public function dayIsNot(string $day): GroupCollectorInterface; + /** * End of the description must match: * @@ -154,6 +173,15 @@ interface GroupCollectorInterface */ public function descriptionEnds(array $array): GroupCollectorInterface; + /** + * End of the description must not match: + * + * @param array $array + * + * @return GroupCollectorInterface + */ + public function descriptionDoesNotEnd(array $array): GroupCollectorInterface; + /** * Description must be: * @@ -163,6 +191,15 @@ interface GroupCollectorInterface */ public function descriptionIs(string $value): GroupCollectorInterface; + /** + * Description must not be: + * + * @param string $value + * + * @return GroupCollectorInterface + */ + public function descriptionIsNot(string $value): GroupCollectorInterface; + /** * Beginning of the description must match: * @@ -172,6 +209,16 @@ interface GroupCollectorInterface */ public function descriptionStarts(array $array): GroupCollectorInterface; + /** + * Beginning of the description must not start with: + * + * @param array $array + * + * @return GroupCollectorInterface + */ + public function descriptionDoesNotStart(array $array): GroupCollectorInterface; + + /** * Exclude destination accounts. * @@ -190,6 +237,15 @@ interface GroupCollectorInterface */ public function excludeSourceAccounts(Collection $accounts): GroupCollectorInterface; + /** + * These accounts must not be accounts. + * + * @param Collection $accounts + * + * @return GroupCollectorInterface + */ + public function excludeAccounts(Collection $accounts): GroupCollectorInterface; + /** * @param string $externalId * @return GroupCollectorInterface @@ -242,6 +298,15 @@ interface GroupCollectorInterface */ public function foreignAmountIs(string $amount): GroupCollectorInterface; + /** + * Get transactions with a specific foreign amount. + * + * @param string $amount + * + * @return GroupCollectorInterface + */ + public function foreignAmountIsNot(string $amount): GroupCollectorInterface; + /** * Get transactions where the amount is less than. * @@ -339,6 +404,13 @@ interface GroupCollectorInterface */ public function metaDayIs(string $day, string $field): GroupCollectorInterface; + /** + * @param string $day + * @param string $field + * @return GroupCollectorInterface + */ + public function metaDayIsNot(string $day, string $field): GroupCollectorInterface; + /** * @param string $month * @param string $field @@ -360,6 +432,13 @@ interface GroupCollectorInterface */ public function metaMonthIs(string $month, string $field): GroupCollectorInterface; + /** + * @param string $month + * @param string $field + * @return GroupCollectorInterface + */ + public function metaMonthIsNot(string $month, string $field): GroupCollectorInterface; + /** * @param string $year * @param string $field @@ -381,6 +460,14 @@ interface GroupCollectorInterface */ public function metaYearIs(string $year, string $field): GroupCollectorInterface; + + /** + * @param string $year + * @param string $field + * @return GroupCollectorInterface + */ + public function metaYearIsNot(string $year, string $field): GroupCollectorInterface; + /** * @param string $month * @return GroupCollectorInterface @@ -399,6 +486,12 @@ interface GroupCollectorInterface */ public function monthIs(string $month): GroupCollectorInterface; + /** + * @param string $month + * @return GroupCollectorInterface + */ + public function monthIsNot(string $month): GroupCollectorInterface; + /** * @param string $value * @@ -406,6 +499,13 @@ interface GroupCollectorInterface */ public function notesContain(string $value): GroupCollectorInterface; + /** + * @param string $value + * + * @return GroupCollectorInterface + */ + public function notesDoNotContain(string $value): GroupCollectorInterface; + /** * @param string $value * @@ -413,6 +513,13 @@ interface GroupCollectorInterface */ public function notesEndWith(string $value): GroupCollectorInterface; + /** + * @param string $value + * + * @return GroupCollectorInterface + */ + public function notesDontEndWith(string $value): GroupCollectorInterface; + /** * @param string $value * @@ -420,6 +527,13 @@ interface GroupCollectorInterface */ public function notesExactly(string $value): GroupCollectorInterface; + /** + * @param string $value + * + * @return GroupCollectorInterface + */ + public function notesExactlyNot(string $value): GroupCollectorInterface; + /** * @param string $value * @@ -427,6 +541,12 @@ interface GroupCollectorInterface */ public function notesStartWith(string $value): GroupCollectorInterface; + /** + * @param string $value + * @return GroupCollectorInterface + */ + public function notesDontStartWith(string $value): GroupCollectorInterface; + /** * @param string $day * @param string $field @@ -448,6 +568,13 @@ interface GroupCollectorInterface */ public function objectDayIs(string $day, string $field): GroupCollectorInterface; + /** + * @param string $day + * @param string $field + * @return GroupCollectorInterface + */ + public function objectDayIsNot(string $day, string $field): GroupCollectorInterface; + /** * @param string $month * @param string $field @@ -469,6 +596,13 @@ interface GroupCollectorInterface */ public function objectMonthIs(string $month, string $field): GroupCollectorInterface; + /** + * @param string $month + * @param string $field + * @return GroupCollectorInterface + */ + public function objectMonthIsNot(string $month, string $field): GroupCollectorInterface; + /** * @param string $year * @param string $field @@ -490,6 +624,13 @@ interface GroupCollectorInterface */ public function objectYearIs(string $year, string $field): GroupCollectorInterface; + /** + * @param string $year + * @param string $field + * @return GroupCollectorInterface + */ + public function objectYearIsNot(string $year, string $field): GroupCollectorInterface; + /** * Define which accounts can be part of the source and destination transactions. * @@ -499,6 +640,18 @@ interface GroupCollectorInterface */ public function setAccounts(Collection $accounts): GroupCollectorInterface; + + /** + * Define which accounts can NOT be part of the source and destination transactions. + * + * @param Collection $accounts + * + * @return GroupCollectorInterface + */ + public function setNotAccounts(Collection $accounts): GroupCollectorInterface; + + + /** * Collect transactions after a specific date. * @@ -535,6 +688,15 @@ interface GroupCollectorInterface */ public function setBills(Collection $bills): GroupCollectorInterface; + /** + * Exclude a specific set of bills + * + * @param Collection $bills + * + * @return GroupCollectorInterface + */ + public function excludeBills(Collection $bills): GroupCollectorInterface; + /** * Both source AND destination must be in this list of accounts. * @@ -553,6 +715,15 @@ interface GroupCollectorInterface */ public function setBudget(Budget $budget): GroupCollectorInterface; + /** + * Exclude a budget + * + * @param Budget $budget + * + * @return GroupCollectorInterface + */ + public function excludeBudget(Budget $budget): GroupCollectorInterface; + /** * Limit the search to a specific set of budgets. * @@ -562,6 +733,15 @@ interface GroupCollectorInterface */ public function setBudgets(Collection $budgets): GroupCollectorInterface; + /** + * Exclude a budget. + * + * @param Collection $budgets + * + * @return GroupCollectorInterface + */ + public function excludeBudgets(Collection $budgets): GroupCollectorInterface; + /** * Limit the search to a specific bunch of categories. * @@ -572,13 +752,12 @@ interface GroupCollectorInterface public function setCategories(Collection $categories): GroupCollectorInterface; /** - * Limit the search not to have a specific bunch of categories. + * Exclude a set of categories. * * @param Collection $categories - * * @return GroupCollectorInterface */ - public function setNotCategories(Collection $categories): GroupCollectorInterface; + public function excludeCategories(Collection $categories): GroupCollectorInterface; /** * Limit the search to a specific category. @@ -589,6 +768,15 @@ interface GroupCollectorInterface */ public function setCategory(Category $category): GroupCollectorInterface; + /** + * Exclude a specific category + * + * @param Category $category + * + * @return GroupCollectorInterface + */ + public function excludeCategory(Category $category): GroupCollectorInterface; + /** * Collect transactions created on a specific date. * @@ -607,6 +795,15 @@ interface GroupCollectorInterface */ public function setCurrency(TransactionCurrency $currency): GroupCollectorInterface; + /** + * Limit results to NOT a specific currency, either foreign or normal one. + * + * @param TransactionCurrency $currency + * + * @return GroupCollectorInterface + */ + public function excludeCurrency(TransactionCurrency $currency): GroupCollectorInterface; + /** * Set destination accounts. * @@ -640,6 +837,15 @@ interface GroupCollectorInterface */ public function setForeignCurrency(TransactionCurrency $currency): GroupCollectorInterface; + /** + * Limit results to exclude a specific foreign currency. + * + * @param TransactionCurrency $currency + * + * @return GroupCollectorInterface + */ + public function excludeForeignCurrency(TransactionCurrency $currency): GroupCollectorInterface; + /** * Limit the result to a set of specific transaction groups. * @@ -649,6 +855,15 @@ interface GroupCollectorInterface */ public function setIds(array $groupIds): GroupCollectorInterface; + /** + * Limit the result to NOT a set of specific transaction groups. + * + * @param array $groupIds + * + * @return GroupCollectorInterface + */ + public function excludeIds(array $groupIds): GroupCollectorInterface; + /** * Look for specific external ID's. * @@ -667,6 +882,15 @@ interface GroupCollectorInterface */ public function setJournalIds(array $journalIds): GroupCollectorInterface; + /** + * Limit the result to NOT a set of specific transaction journals. + * + * @param array $journalIds + * + * @return GroupCollectorInterface + */ + public function excludeJournalIds(array $journalIds): GroupCollectorInterface; + /** * Limit the number of returned entries. * @@ -705,6 +929,16 @@ interface GroupCollectorInterface */ public function setMetaDateRange(Carbon $start, Carbon $end, string $field): GroupCollectorInterface; + /** + * @param Carbon $start + * @param Carbon $end + * @param string $field + * @return GroupCollectorInterface + */ + public function excludeMetaDateRange(Carbon $start, Carbon $end, string $field): GroupCollectorInterface; + + + /** * @param Carbon $date * @param string $field @@ -727,6 +961,14 @@ interface GroupCollectorInterface */ public function setObjectRange(Carbon $start, Carbon $end, string $field): GroupCollectorInterface; + /** + * @param Carbon $start + * @param Carbon $end + * @param string $field + * @return GroupCollectorInterface + */ + public function excludeObjectRange(Carbon $start, Carbon $end, string $field): GroupCollectorInterface; + /** * Set the page to get. * @@ -746,6 +988,13 @@ interface GroupCollectorInterface */ public function setRange(Carbon $start, Carbon $end): GroupCollectorInterface; + /** + * @param Carbon $start + * @param Carbon $end + * @return GroupCollectorInterface + */ + public function excludeRange(Carbon $start, Carbon $end): GroupCollectorInterface; + /** * Look for specific recurring ID's. * @@ -764,6 +1013,15 @@ interface GroupCollectorInterface */ public function setSearchWords(array $array): GroupCollectorInterface; + /** + * Exclude words in descriptions. + * + * @param array $array + * + * @return GroupCollectorInterface + */ + public function excludeSearchWords(array $array): GroupCollectorInterface; + /** * Set source accounts. * @@ -809,6 +1067,15 @@ interface GroupCollectorInterface */ public function setTypes(array $types): GroupCollectorInterface; + /** + * Limit the included transaction types. + * + * @param array $types + * + * @return GroupCollectorInterface + */ + public function excludeTypes(array $types): GroupCollectorInterface; + /** * Collect transactions updated on a specific date. * @@ -1000,5 +1267,11 @@ interface GroupCollectorInterface */ public function yearIs(string $year): GroupCollectorInterface; + /** + * @param string $year + * @return GroupCollectorInterface + */ + public function yearIsNot(string $year): GroupCollectorInterface; + } diff --git a/app/Support/Search/OperatorQuerySearch.php b/app/Support/Search/OperatorQuerySearch.php index a0075bf504..5b832dc8d4 100644 --- a/app/Support/Search/OperatorQuerySearch.php +++ b/app/Support/Search/OperatorQuerySearch.php @@ -78,6 +78,7 @@ class OperatorQuerySearch implements SearchInterface private TagRepositoryInterface $tagRepository; private array $validOperators; private array $words; + private array $prohibitedWords; /** * OperatorQuerySearch constructor. @@ -90,6 +91,7 @@ class OperatorQuerySearch implements SearchInterface $this->operators = new Collection; $this->page = 1; $this->words = []; + $this->prohibitedWords = []; $this->invalidOperators = []; $this->limit = 25; $this->date = today(config('app.timezone')); @@ -169,6 +171,7 @@ class OperatorQuerySearch implements SearchInterface } $this->collector->setSearchWords($this->words); + $this->collector->excludeSearchWords($this->prohibitedWords); } /** @@ -210,6 +213,13 @@ class OperatorQuerySearch implements SearchInterface $operator = strtolower($searchNode->getValue()); $value = $searchNode->getNode()->getValue(); $prohibited = $searchNode->getBoolOperator()->equals(BoolOperator::PROHIBITED()->getValue()); + $context = config(sprintf('search.operators.%s.needs_context', $operator)); + + // is an operator that needs no context, and value is false, then prohibited = true. + if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context) { + $prohibited = true; + } + // must be valid operator: if ( in_array($operator, $this->validOperators, true) && @@ -244,6 +254,7 @@ 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); } @@ -267,51 +278,99 @@ class OperatorQuerySearch implements SearchInterface case 'account_is': $this->searchAccount($value, 3, 4); break; + case '!account_is': + $this->searchAccount($value, 3, 4, true); + break; case 'account_contains': $this->searchAccount($value, 3, 3); break; + case '!account_contains': + $this->searchAccount($value, 3, 3, true); + break; case 'account_ends': $this->searchAccount($value, 3, 2); break; + case '!account_ends': + $this->searchAccount($value, 3, 2, true); + break; case 'account_starts': $this->searchAccount($value, 3, 1); break; + case '!account_starts': + $this->searchAccount($value, 3, 1, true); + break; case 'account_nr_is': $this->searchAccountNr($value, 3, 4); break; + 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': + $this->searchAccountNr($value, 3, 3, true); + break; case 'account_nr_ends': $this->searchAccountNr($value, 3, 2); break; + 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': + $this->searchAccountNr($value, 3, 1, true); + break; case 'source_account_starts': $this->searchAccount($value, 1, 1); break; + 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': + $this->searchAccount($value, 1, 2, true); + break; case 'source_account_is': $this->searchAccount($value, 1, 4); break; + 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': + $this->searchAccountNr($value, 1, 1, true); + break; case 'source_account_nr_ends': $this->searchAccountNr($value, 1, 2); break; + 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': + $this->searchAccountNr($value, 1, 4, true); + break; case 'source_account_nr_contains': $this->searchAccountNr($value, 1, 3); break; + 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': + $this->searchAccount($value, 1, 3, true); + break; case 'source_account_id': $account = $this->accountRepository->find((int) $value); if (null !== $account) { @@ -322,38 +381,80 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!source_account_id': + $account = $this->accountRepository->find((int) $value); + if (null !== $account) { + $this->collector->excludeSourceAccounts(new Collection([$account])); + } + if (null === $account) { + // since the source does not exist, cannot return results: + $this->collector->findNothing(); + } + break; case 'journal_id': $parts = explode(',', $value); $this->collector->setJournalIds($parts); break; + case '!journal_id': + $parts = explode(',', $value); + $this->collector->excludeJournalIds($parts); + break; case 'id': $parts = explode(',', $value); $this->collector->setIds($parts); break; + case '!id': + $parts = explode(',', $value); + $this->collector->excludeIds($parts); + break; case 'destination_account_starts': $this->searchAccount($value, 2, 1); break; + 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': + $this->searchAccount($value, 2, 2, true); + break; case 'destination_account_nr_starts': $this->searchAccountNr($value, 2, 1); break; + 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': + $this->searchAccountNr($value, 2, 2, true); + break; case 'destination_account_nr_is': $this->searchAccountNr($value, 2, 4); break; + 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': + $this->searchAccount($value, 2, 4, true); + break; case 'destination_account_nr_contains': $this->searchAccountNr($value, 2, 3); break; + 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': + $this->searchAccount($value, 2, 3, true); + break; case 'destination_account_id': $account = $this->accountRepository->find((int) $value); if (null !== $account) { @@ -363,6 +464,15 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!destination_account_id': + $account = $this->accountRepository->find((int) $value); + if (null !== $account) { + $this->collector->excludeDestinationAccounts(new Collection([$account])); + } + if (null === $account) { + $this->collector->findNothing(); + } + break; case 'account_id': $parts = explode(',', $value); $collection = new Collection; @@ -379,6 +489,22 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!account_id': + $parts = explode(',', $value); + $collection = new Collection; + foreach ($parts as $accountId) { + $account = $this->accountRepository->find((int) $accountId); + if (null !== $account) { + $collection->push($account); + } + } + if ($collection->count() > 0) { + $this->collector->setNotAccounts($collection); + } + if (0 === $collection->count()) { + $this->collector->findNothing(); + } + break; // // cash account // @@ -386,30 +512,55 @@ class OperatorQuerySearch implements SearchInterface $account = $this->getCashAccount(); $this->collector->setSourceAccounts(new Collection([$account])); break; + case '!source_is_cash': + $account = $this->getCashAccount(); + $this->collector->excludeSourceAccounts(new Collection([$account])); + break; case 'destination_is_cash': $account = $this->getCashAccount(); $this->collector->setDestinationAccounts(new Collection([$account])); break; + case '!destination_is_cash': + $account = $this->getCashAccount(); + $this->collector->excludeDestinationAccounts(new Collection([$account])); + break; case 'account_is_cash': $account = $this->getCashAccount(); $this->collector->setAccounts(new Collection([$account])); break; + case '!account_is_cash': + $account = $this->getCashAccount(); + $this->collector->excludeAccounts(new Collection([$account])); + break; // // description // case 'description_starts': $this->collector->descriptionStarts([$value]); break; + case '!description_starts': + $this->collector->descriptionDoesNotStart([$value]); + break; case 'description_ends': $this->collector->descriptionEnds([$value]); break; + case '!description_ends': + $this->collector->descriptionDoesNotEnd([$value]); + break; case 'description_contains': $this->words[] = $value; return false; + case '!description_contains': + $this->prohibitedWords[] = $value; + + break; case 'description_is': $this->collector->descriptionIs($value); break; + case '!description_is': + $this->collector->descriptionIsNot($value); + break; // // currency // @@ -422,6 +573,15 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!currency_is': + $currency = $this->findCurrency($value); + if (null !== $currency) { + $this->collector->excludeCurrency($currency); + } + if (null === $currency) { + $this->collector->findNothing(); + } + break; case 'foreign_currency_is': $currency = $this->findCurrency($value); if (null !== $currency) { @@ -431,22 +591,35 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!foreign_currency_is': + $currency = $this->findCurrency($value); + if (null !== $currency) { + $this->collector->excludeForeignCurrency($currency); + } + if (null === $currency) { + $this->collector->findNothing(); + } + break; // // attachments // case 'has_attachments': + case '!has_no_attachments': Log::debug('Set collector to filter on attachments.'); $this->collector->hasAttachments(); break; case 'has_no_attachments': + case '!has_attachments': Log::debug('Set collector to filter on NO attachments.'); $this->collector->hasNoAttachments(); break; // // categories + case '!has_any_category': case 'has_no_category': $this->collector->withoutCategory(); break; + case '!has_no_category': case 'has_any_category': $this->collector->withCategory(); break; @@ -458,6 +631,13 @@ class OperatorQuerySearch implements SearchInterface } $this->collector->findNothing(); break; + case '!category_is': + $category = $this->categoryRepository->findByName($value); + if (null !== $category) { + $this->collector->excludeCategory($category); + break; + } + break; case 'category_ends': $result = $this->categoryRepository->categoryEndsWith($value, 1337); if ($result->count() > 0) { @@ -467,6 +647,15 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!category_ends': + $result = $this->categoryRepository->categoryEndsWith($value, 1337); + if ($result->count() > 0) { + $this->collector->excludeCategories($result); + } + if (0 === $result->count()) { + $this->collector->findNothing(); + } + break; case 'category_starts': $result = $this->categoryRepository->categoryStartsWith($value, 1337); if ($result->count() > 0) { @@ -476,6 +665,15 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!category_starts': + $result = $this->categoryRepository->categoryStartsWith($value, 1337); + if ($result->count() > 0) { + $this->collector->excludeCategories($result); + } + if (0 === $result->count()) { + $this->collector->findNothing(); + } + break; case 'category_contains': $result = $this->categoryRepository->searchCategory($value, 1337); if ($result->count() > 0) { @@ -488,7 +686,7 @@ class OperatorQuerySearch implements SearchInterface case '!category_contains': $result = $this->categoryRepository->searchCategory($value, 1337); if ($result->count() > 0) { - $this->collector->setNotCategories($result); + $this->collector->excludeCategories($result); } if (0 === $result->count()) { $this->collector->findNothing(); @@ -497,10 +695,12 @@ class OperatorQuerySearch implements SearchInterface // // budgets // + case '!has_any_budget': case 'has_no_budget': $this->collector->withoutBudget(); break; case 'has_any_budget': + case '!has_no_budget': $this->collector->withBudget(); break; case 'budget_contains': @@ -512,6 +712,15 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!budget_contains': + $result = $this->budgetRepository->searchBudget($value, 1337); + if ($result->count() > 0) { + $this->collector->excludeBudgets($result); + } + if (0 === $result->count()) { + $this->collector->findNothing(); + } + break; case 'budget_is': $budget = $this->budgetRepository->findByName($value); if (null !== $budget) { @@ -520,6 +729,14 @@ class OperatorQuerySearch implements SearchInterface } $this->collector->findNothing(); break; + case '!budget_is': + $budget = $this->budgetRepository->findByName($value); + if (null !== $budget) { + $this->collector->excludeBudget($budget); + break; + } + $this->collector->findNothing(); + break; case 'budget_ends': $result = $this->budgetRepository->budgetEndsWith($value, 1337); if ($result->count() > 0) { @@ -529,6 +746,15 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!budget_ends': + $result = $this->budgetRepository->budgetEndsWith($value, 1337); + if ($result->count() > 0) { + $this->collector->excludeBudgets($result); + } + if (0 === $result->count()) { + $this->collector->findNothing(); + } + break; case 'budget_starts': $result = $this->budgetRepository->budgetStartsWith($value, 1337); if ($result->count() > 0) { @@ -538,12 +764,23 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!budget_starts': + $result = $this->budgetRepository->budgetStartsWith($value, 1337); + if ($result->count() > 0) { + $this->collector->excludeBudgets($result); + } + if (0 === $result->count()) { + $this->collector->findNothing(); + } + break; // // bill // + case '!has_any_bill': case 'has_no_bill': $this->collector->withoutBill(); break; + case '!has_no_bill': case 'has_any_bill': $this->collector->withBill(); break; @@ -555,6 +792,14 @@ class OperatorQuerySearch implements SearchInterface } $this->collector->findNothing(); break; + case '!bill_contains': + $result = $this->billRepository->searchBill($value, 1337); + if ($result->count() > 0) { + $this->collector->excludeBills($result); + break; + } + $this->collector->findNothing(); + break; case 'bill_is': $bill = $this->billRepository->findByName($value); if (null !== $bill) { @@ -563,6 +808,14 @@ class OperatorQuerySearch implements SearchInterface } $this->collector->findNothing(); break; + case '!bill_is': + $bill = $this->billRepository->findByName($value); + if (null !== $bill) { + $this->collector->excludeBills(new Collection([$bill])); + break; + } + $this->collector->findNothing(); + break; case 'bill_ends': $result = $this->billRepository->billEndsWith($value, 1337); if ($result->count() > 0) { @@ -572,6 +825,15 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!bill_ends': + $result = $this->billRepository->billEndsWith($value, 1337); + if ($result->count() > 0) { + $this->collector->excludeBills($result); + } + if (0 === $result->count()) { + $this->collector->findNothing(); + } + break; case 'bill_starts': $result = $this->billRepository->billStartsWith($value, 1337); if ($result->count() > 0) { @@ -581,15 +843,27 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!bill_starts': + $result = $this->billRepository->billStartsWith($value, 1337); + if ($result->count() > 0) { + $this->collector->excludeBills($result); + } + if (0 === $result->count()) { + $this->collector->findNothing(); + } + break; // // tags // + case '!has_any_tag': case 'has_no_tag': $this->collector->withoutTags(); break; + case '!has_no_tag': case 'has_any_tag': $this->collector->hasAnyTag(); break; + case '!tag_is_not': case 'tag_is': $result = $this->tagRepository->searchTag($value); if ($result->count() > 0) { @@ -601,6 +875,7 @@ class OperatorQuerySearch implements SearchInterface $this->collector->findNothing(); } break; + case '!tag_is': case 'tag_is_not': $result = $this->tagRepository->searchTag($value); if ($result->count() > 0) { @@ -613,24 +888,41 @@ class OperatorQuerySearch implements SearchInterface case 'notes_contains': $this->collector->notesContain($value); break; + case '!notes_contains': + $this->collector->notesDoNotContain($value); + break; case 'notes_starts': $this->collector->notesStartWith($value); break; + case '!notes_starts': + $this->collector->notesDontStartWith($value); + break; case 'notes_ends': $this->collector->notesEndWith($value); break; + case '!notes_ends': + $this->collector->notesDontEndWith($value); + break; case 'notes_is': $this->collector->notesExactly($value); break; + case '!notes_is': + $this->collector->notesExactlyNot($value); + break; + case '!any_notes': case 'no_notes': $this->collector->withoutNotes(); break; case 'any_notes': + case '!no_notes': $this->collector->withAnyNotes(); break; case 'reconciled': $this->collector->isReconciled(); break; + case '!reconciled': + $this->collector->isNotReconciled(); + break; // // amount // @@ -642,6 +934,14 @@ class OperatorQuerySearch implements SearchInterface Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); $this->collector->amountIs($amount); break; + case '!amount_is': + // strip comma's, make dots. + Log::debug(sprintf('Original value "%s"', $value)); + $value = str_replace(',', '.', (string) $value); + $amount = app('steam')->positive($value); + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); + $this->collector->amountIsNot($amount); + break; case 'foreign_amount_is': // strip comma's, make dots. @@ -651,6 +951,16 @@ 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': + + // strip comma's, make dots. + $value = str_replace(',', '.', (string) $value); + + $amount = app('steam')->positive($value); + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); + $this->collector->foreignAmountIsNot($amount); + break; + case '!amount_more': case 'amount_less': // strip comma's, make dots. $value = str_replace(',', '.', (string) $value); @@ -659,6 +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_less': // strip comma's, make dots. $value = str_replace(',', '.', (string) $value); @@ -667,6 +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_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. @@ -675,6 +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_more': Log::debug(sprintf('Now handling operator "%s"', $operator)); // strip comma's, make dots. @@ -690,121 +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': + $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': $range = $this->parseDateRange($value); - $this->setExactDateParams($range); + $this->setExactDateParams($range, $prohibited); return false; case 'date_before': + case '!date_after': $range = $this->parseDateRange($value); $this->setDateBeforeParams($range); return false; case 'date_after': + case '!date_before': $range = $this->parseDateRange($value); $this->setDateAfterParams($range); return false; + case 'interest_date_on': + case '!interest_date_on': $range = $this->parseDateRange($value); - $this->setExactMetaDateParams('interest_date', $range); + $this->setExactMetaDateParams('interest_date', $range, $prohibited); return false; case 'interest_date_before': + case '!interest_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('interest_date', $range); return false; case 'interest_date_after': + case '!interest_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('interest_date', $range); return false; case 'book_date_on': + case '!book_date_on': $range = $this->parseDateRange($value); - $this->setExactMetaDateParams('book_date', $range); + $this->setExactMetaDateParams('book_date', $range, $prohibited); return false; case 'book_date_before': + case '!book_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('book_date', $range); return false; case 'book_date_after': + case '!book_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('book_date', $range); return false; case 'process_date_on': + case '!process_date_on': $range = $this->parseDateRange($value); - $this->setExactMetaDateParams('process_date', $range); + $this->setExactMetaDateParams('process_date', $range, $prohibited); return false; case 'process_date_before': + case '!process_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('process_date', $range); return false; case 'process_date_after': + case '!process_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('process_date', $range); return false; + case 'due_date_on': + case '!due_date_on': $range = $this->parseDateRange($value); - $this->setExactMetaDateParams('due_date', $range); + $this->setExactMetaDateParams('due_date', $range, $prohibited); return false; case 'due_date_before': + case '!due_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('due_date', $range); return false; case 'due_date_after': + case '!due_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('due_date', $range); return false; + case 'payment_date_on': + case '!payment_date_on': $range = $this->parseDateRange($value); - $this->setExactMetaDateParams('payment_date', $range); + $this->setExactMetaDateParams('payment_date', $range, $prohibited); return false; case 'payment_date_before': + case '!payment_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('payment_date', $range); return false; case 'payment_date_after': + case '!payment_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('payment_date', $range); return false; + case 'invoice_date_on': + case '!invoice_date_on': $range = $this->parseDateRange($value); - $this->setExactMetaDateParams('invoice_date', $range); + $this->setExactMetaDateParams('invoice_date', $range, $prohibited); return false; case 'invoice_date_before': + case '!invoice_date_after': $range = $this->parseDateRange($value); $this->setMetaDateBeforeParams('invoice_date', $range); return false; case 'invoice_date_after': + case '!invoice_date_before': $range = $this->parseDateRange($value); $this->setMetaDateAfterParams('invoice_date', $range); return false; + 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); + $this->setExactObjectDateParams('created_at', $range, $prohibited); return false; case 'created_at_before': + 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': 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': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); $range = $this->parseDateRange($value); - $this->setExactObjectDateParams('updated_at', $range); + $this->setExactObjectDateParams('updated_at', $range, $prohibited); return false; case 'updated_at_before': + 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': Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); $range = $this->parseDateRange($value); $this->setObjectDateAfterParams('updated_at', $range); @@ -812,9 +1162,11 @@ class OperatorQuerySearch implements SearchInterface // // external URL // + case '!any_external_url': case 'no_external_url': $this->collector->withoutExternalUrl(); break; + case '!no_external_url': case 'any_external_url': $this->collector->withExternalUrl(); break; @@ -848,6 +1200,7 @@ class OperatorQuerySearch implements SearchInterface case 'external_id_ends': $this->collector->externalIdEnds($value); break; + case 'internal_reference_is': $this->collector->setInternalReference($value); break; @@ -860,6 +1213,7 @@ class OperatorQuerySearch implements SearchInterface case 'internal_reference_ends': $this->collector->internalReferenceEnds($value); break; + case 'attachment_name_is': $this->collector->attachmentNameIs($value); break; @@ -872,6 +1226,7 @@ class OperatorQuerySearch implements SearchInterface case 'attachment_name_ends': $this->collector->attachmentNameEnds($value); break; + case 'attachment_notes_are': $this->collector->attachmentNotesAre($value); break; @@ -884,12 +1239,12 @@ class OperatorQuerySearch implements SearchInterface case 'attachment_notes_ends': $this->collector->attachmentNotesEnds($value); break; + case 'exists': $this->collector->exists(); break; } - return true; } @@ -901,18 +1256,29 @@ 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 + // the configuration from the original operator. + if (str_starts_with($operator, '!')) { + $operator = substr($operator, 1); + } + $config = config(sprintf('search.operators.%s', $operator)); if (null === $config) { throw new FireflyException(sprintf('No configuration for search operator "%s"', $operator)); } if (true === $config['alias']) { - Log::debug(sprintf('"%s" is an alias for "%s", so return that instead.', $operator, $config['alias_for'])); + $return = $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)); - return $config['alias_for']; + return $return; } Log::debug(sprintf('"%s" is not an alias.', $operator)); - return $operator; + return $original; } /** @@ -922,25 +1288,35 @@ class OperatorQuerySearch implements SearchInterface * @param string $value * @param int $searchDirection * @param int $stringPosition + * @param bool $prohibited */ - private function searchAccount(string $value, int $searchDirection, int $stringPosition): void + private function searchAccount(string $value, int $searchDirection, int $stringPosition, bool $prohibited = false): void { Log::debug(sprintf('searchAccount("%s", %d, %d)', $value, $stringPosition, $searchDirection)); // search direction (default): for source accounts $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE]; $collectorMethod = 'setSourceAccounts'; + if ($prohibited) { + $collectorMethod = 'excludeSourceAccounts'; + } // search direction: for destination accounts if (2 === $searchDirection) { // destination can be $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE]; $collectorMethod = 'setDestinationAccounts'; + if ($prohibited) { + $collectorMethod = 'excludeDestinationAccounts'; + } } // either account could be: if (3 === $searchDirection) { $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE, AccountType::REVENUE]; $collectorMethod = 'setAccounts'; + if ($prohibited) { + $collectorMethod = 'excludeAccounts'; + } } // string position (default): starts with: $stringMethod = 'str_starts_with'; @@ -988,26 +1364,36 @@ class OperatorQuerySearch implements SearchInterface * @param string $value * @param int $searchDirection * @param int $stringPosition + * @param bool $prohibited */ - private function searchAccountNr(string $value, int $searchDirection, int $stringPosition): void + private function searchAccountNr(string $value, int $searchDirection, int $stringPosition, bool $prohibited = false): void { Log::debug(sprintf('searchAccountNr(%s, %d, %d)', $value, $searchDirection, $stringPosition)); // search direction (default): for source accounts $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE]; $collectorMethod = 'setSourceAccounts'; + if (true === $prohibited) { + $collectorMethod = 'excludeSourceAccounts'; + } // search direction: for destination accounts if (2 === $searchDirection) { // destination can be $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE]; $collectorMethod = 'setDestinationAccounts'; + if (true === $prohibited) { + $collectorMethod = 'excludeDestinationAccounts'; + } } // either account could be: if (3 === $searchDirection) { $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE, AccountType::REVENUE]; $collectorMethod = 'setAccounts'; + if (true === $prohibited) { + $collectorMethod = 'excludeAccounts'; + } } // string position (default): starts with: @@ -1113,13 +1499,14 @@ class OperatorQuerySearch implements SearchInterface * * @throws FireflyException */ - private function setExactDateParams(array $range): void + private function setExactDateParams(array $range, bool $prohibited = false): void { /** * @var string $key * @var Carbon|string $value */ foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; switch ($key) { default: throw new FireflyException(sprintf('Cannot handle key "%s" in setExactParameters()', $key)); @@ -1128,21 +1515,40 @@ class OperatorQuerySearch implements SearchInterface $this->collector->setRange($value, $value); $this->operators->push(['type' => 'date_on', 'value' => $value->format('Y-m-d'),]); break; + case 'exact_not': + $this->collector->excludeRange($value, $value); + $this->operators->push(['type' => 'not_date_on', 'value' => $value->format('Y-m-d'),]); + break; case 'year': Log::debug(sprintf('Set date_is_exact YEAR value "%s"', $value)); $this->collector->yearIs($value); $this->operators->push(['type' => 'date_on_year', 'value' => $value,]); break; + case 'year_not': + Log::debug(sprintf('Set date_is_exact_not YEAR value "%s"', $value)); + $this->collector->yearIsNot($value); + $this->operators->push(['type' => 'not_date_on_year', 'value' => $value,]); + break; case 'month': Log::debug(sprintf('Set date_is_exact MONTH value "%s"', $value)); $this->collector->monthIs($value); $this->operators->push(['type' => 'date_on_month', 'value' => $value,]); break; + case 'month_not': + Log::debug(sprintf('Set date_is_exact not MONTH value "%s"', $value)); + $this->collector->monthIsNot($value); + $this->operators->push(['type' => 'not_date_on_month', 'value' => $value,]); + break; case 'day': Log::debug(sprintf('Set date_is_exact DAY value "%s"', $value)); $this->collector->dayIs($value); $this->operators->push(['type' => 'date_on_day', 'value' => $value,]); break; + case 'day_not': + Log::debug(sprintf('Set not date_is_exact DAY value "%s"', $value)); + $this->collector->dayIsNot($value); + $this->operators->push(['type' => 'not_date_on_day', 'value' => $value,]); + break; } } } @@ -1152,13 +1558,14 @@ class OperatorQuerySearch implements SearchInterface * * @throws FireflyException */ - private function setDateBeforeParams(array $range): void + private function setDateBeforeParams(array $range, bool $prohibited = false): void { /** * @var string $key * @var Carbon|string $value */ foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; switch ($key) { default: throw new FireflyException(sprintf('Cannot handle key "%s" in setDateBeforeParams()', $key)); @@ -1190,13 +1597,14 @@ class OperatorQuerySearch implements SearchInterface * * @throws FireflyException */ - private function setDateAfterParams(array $range) + private function setDateAfterParams(array $range, bool $prohibited = false) { /** * @var string $key * @var Carbon|string $value */ foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; switch ($key) { default: throw new FireflyException(sprintf('Cannot handle key "%s" in setDateAfterParams()', $key)); @@ -1229,7 +1637,7 @@ class OperatorQuerySearch implements SearchInterface * @return void * @throws FireflyException */ - private function setExactMetaDateParams(string $field, array $range): void + private function setExactMetaDateParams(string $field, array $range, bool $prohibited = false): void { Log::debug('Now in setExactMetaDateParams()'); /** @@ -1237,6 +1645,7 @@ class OperatorQuerySearch implements SearchInterface * @var Carbon|string $value */ foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; switch ($key) { default: throw new FireflyException(sprintf('Cannot handle key "%s" in setExactMetaDateParams()', $key)); @@ -1245,21 +1654,41 @@ class OperatorQuerySearch implements SearchInterface $this->collector->setMetaDateRange($value, $value, $field); $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d'),]); break; + case 'exact_not': + Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->excludeMetaDateRange($value, $value, $field); + $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d'),]); + break; case 'year': Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); $this->collector->metaYearIs($value, $field); $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value,]); break; + case 'year_not': + Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->metaYearIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value,]); + break; case 'month': Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); $this->collector->metaMonthIs($value, $field); $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value,]); break; + case 'month_not': + Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->metaMonthIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value,]); + break; case 'day': Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); $this->collector->metaDayIs($value, $field); $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value,]); break; + case 'day_not': + Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->metaDayIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value,]); + break; } } } @@ -1270,13 +1699,14 @@ class OperatorQuerySearch implements SearchInterface * @return void * @throws FireflyException */ - private function setMetaDateBeforeParams(string $field, array $range): void + private function setMetaDateBeforeParams(string $field, array $range, bool $prohibited = false): void { /** * @var string $key * @var Carbon|string $value */ foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; switch ($key) { default: throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateBeforeParams()', $key)); @@ -1309,13 +1739,14 @@ class OperatorQuerySearch implements SearchInterface * @return void * @throws FireflyException */ - private function setMetaDateAfterParams(string $field, array $range): void + private function setMetaDateAfterParams(string $field, array $range, bool $prohibited = false): void { /** * @var string $key * @var Carbon|string $value */ foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; switch ($key) { default: throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateAfterParams()', $key)); @@ -1348,13 +1779,14 @@ class OperatorQuerySearch implements SearchInterface * @return void * @throws FireflyException */ - private function setExactObjectDateParams(string $field, array $range): void + private function setExactObjectDateParams(string $field, array $range, bool $prohibited = false): void { /** * @var string $key * @var Carbon|string $value */ foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; switch ($key) { default: throw new FireflyException(sprintf('Cannot handle key "%s" in setExactObjectDateParams()', $key)); @@ -1363,21 +1795,41 @@ class OperatorQuerySearch implements SearchInterface $this->collector->setObjectRange($value, clone $value, $field); $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d'),]); break; + case 'exact_not': + Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d'))); + $this->collector->excludeObjectRange($value, clone $value, $field); + $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d'),]); + break; case 'year': Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value)); $this->collector->objectYearIs($value, $field); $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value,]); break; + case 'year_not': + Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value)); + $this->collector->objectYearIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value,]); + break; case 'month': Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value)); $this->collector->objectMonthIs($value, $field); $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value,]); break; + case 'month_not': + Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value)); + $this->collector->objectMonthIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value,]); + break; case 'day': Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value)); $this->collector->objectDayIs($value, $field); $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value,]); break; + case 'day_not': + Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value)); + $this->collector->objectDayIsNot($value, $field); + $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value,]); + break; } } } @@ -1388,13 +1840,14 @@ class OperatorQuerySearch implements SearchInterface * * @throws FireflyException */ - private function setObjectDateBeforeParams(string $field, array $range) + private function setObjectDateBeforeParams(string $field, array $range, bool $prohibited = false): void { /** * @var string $key * @var Carbon|string $value */ foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; switch ($key) { default: throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateBeforeParams()', $key)); @@ -1427,13 +1880,14 @@ class OperatorQuerySearch implements SearchInterface * * @throws FireflyException */ - private function setObjectDateAfterParams(string $field, array $range) + private function setObjectDateAfterParams(string $field, array $range, bool $prohibited = false): void { /** * @var string $key * @var Carbon|string $value */ foreach ($range as $key => $value) { + $key = $prohibited ? sprintf('%s_not', $key) : $key; switch ($key) { default: throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateAfterParams()', $key)); diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 7e94c61f64..b2bf5e872a 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -322,97 +322,165 @@ return [ // old - 'search_modifier_date_on' => 'Transaction date is ":value"', - 'search_modifier_reconciled' => 'Transaction is reconciled', - 'search_modifier_id' => 'Transaction ID is ":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_no_external_url' => 'The transaction has no external URL', - 'search_modifier_any_external_url' => 'The transaction must have a (any) external URL', - 'search_modifier_internal_reference_is' => 'Internal reference is ":value"', - 'search_modifier_description_starts' => 'Description is ":value"', - 'search_modifier_description_ends' => 'Description ends with ":value"', - 'search_modifier_description_contains' => 'Description contains ":value"', - 'search_modifier_description_is' => 'Description is exactly ":value"', - 'search_modifier_currency_is' => 'Transaction (foreign) currency is ":value"', - 'search_modifier_foreign_currency_is' => 'Transaction foreign currency is ":value"', - 'search_modifier_has_attachments' => 'The transaction must have an attachment', - 'search_modifier_has_no_category' => 'The transaction must have no category', - 'search_modifier_has_any_category' => 'The transaction must have a (any) category', - 'search_modifier_has_no_budget' => 'The transaction must have no budget', - 'search_modifier_has_any_budget' => 'The transaction must have a (any) budget', - 'search_modifier_has_no_bill' => 'The transaction must have no bill', - 'search_modifier_has_any_bill' => 'The transaction must have a (any) bill', - 'search_modifier_has_no_tag' => 'The transaction must have no tags', - 'search_modifier_has_any_tag' => 'The transaction must have a (any) tag', - 'search_modifier_notes_contains' => 'The transaction notes contain ":value"', - 'search_modifier_notes_starts' => 'The transaction notes start with ":value"', - 'search_modifier_notes_ends' => 'The transaction notes end with ":value"', - 'search_modifier_notes_is' => 'The transaction notes are exactly ":value"', - 'search_modifier_no_notes' => 'The transaction has no notes', - 'search_modifier_any_notes' => 'The transaction must have notes', - 'search_modifier_amount_is' => 'Amount is exactly :value', - 'search_modifier_amount_less' => 'Amount is less than or equal to :value', - 'search_modifier_amount_more' => 'Amount is more than or equal to :value', - 'search_modifier_source_account_is' => 'Source account name is exactly ":value"', - 'search_modifier_source_account_contains' => 'Source account name contains ":value"', - 'search_modifier_source_account_starts' => 'Source account name starts with ":value"', - 'search_modifier_source_account_ends' => 'Source account name ends with ":value"', - 'search_modifier_source_account_id' => 'Source account ID is :value', - 'search_modifier_source_account_nr_is' => 'Source account number (IBAN) is ":value"', - 'search_modifier_source_account_nr_contains' => 'Source account number (IBAN) contains ":value"', - 'search_modifier_source_account_nr_starts' => 'Source account number (IBAN) starts with ":value"', - 'search_modifier_source_account_nr_ends' => 'Source account number (IBAN) ends with ":value"', - 'search_modifier_destination_account_is' => 'Destination account name is exactly ":value"', - 'search_modifier_destination_account_contains' => 'Destination account name contains ":value"', - 'search_modifier_destination_account_starts' => 'Destination account name starts with ":value"', - 'search_modifier_destination_account_ends' => 'Destination account name ends with ":value"', - 'search_modifier_destination_account_id' => 'Destination account ID is :value', - 'search_modifier_destination_is_cash' => 'Destination account is (cash) account', - 'search_modifier_source_is_cash' => 'Source account is (cash) account', - 'search_modifier_destination_account_nr_is' => 'Destination account number (IBAN) is ":value"', - 'search_modifier_destination_account_nr_contains' => 'Destination account number (IBAN) contains ":value"', - 'search_modifier_destination_account_nr_starts' => 'Destination account number (IBAN) starts with ":value"', - 'search_modifier_destination_account_nr_ends' => 'Destination account number (IBAN) ends with ":value"', - 'search_modifier_account_id' => 'Source or destination account ID\'s is/are: :value', - 'search_modifier_category_is' => 'Category is ":value"', - 'search_modifier_budget_is' => 'Budget is ":value"', - 'search_modifier_bill_is' => 'Bill is ":value"', - 'search_modifier_transaction_type' => 'Transaction type is ":value"', - 'search_modifier_tag_is' => 'Tag is ":value"', - 'search_modifier_date_on_year' => 'Transaction is in year ":value"', - 'search_modifier_date_on_month' => 'Transaction is in month ":value"', - 'search_modifier_date_on_day' => 'Transaction is on day of month ":value"', - 'search_modifier_date_before_year' => 'Transaction is before or in year ":value"', - 'search_modifier_date_before_month' => 'Transaction is before or in month ":value"', - 'search_modifier_date_before_day' => 'Transaction is before or on day of month ":value"', - 'search_modifier_date_after_year' => 'Transaction is in or after year ":value"', - 'search_modifier_date_after_month' => 'Transaction is in or after month ":value"', - 'search_modifier_date_after_day' => 'Transaction is after or on day of month ":value"', + 'search_modifier_date_on' => 'Transaction date is ":value"', + 'search_modifier_not_date_on' => 'Transaction date is not ":value"', + 'search_modifier_reconciled' => 'Transaction is reconciled', + 'search_modifier_not_reconciled' => 'Transaction is not reconciled', + 'search_modifier_id' => 'Transaction ID is ":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_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_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"', + 'search_modifier_not_description_ends' => 'Description does not end on ":value"', + 'search_modifier_description_contains' => 'Description contains ":value"', + 'search_modifier_not_description_contains' => 'Description does not contain ":value"', + 'search_modifier_description_is' => 'Description is exactly ":value"', + 'search_modifier_not_description_is' => 'Description is exactly not ":value"', + 'search_modifier_currency_is' => 'Transaction (foreign) currency is ":value"', + 'search_modifier_not_currency_is' => 'Transaction (foreign) currency is not ":value"', + 'search_modifier_foreign_currency_is' => 'Transaction foreign currency is ":value"', + 'search_modifier_not_foreign_currency_is' => 'Transaction foreign currency is not ":value"', + 'search_modifier_has_attachments' => 'The transaction must have an attachment', + 'search_modifier_has_no_category' => 'The transaction must have no category', + 'search_modifier_not_has_no_category' => 'The transaction must have a (any) category', + 'search_modifier_not_has_any_category' => 'The transaction must have no category', + 'search_modifier_has_any_category' => 'The transaction must have a (any) category', + 'search_modifier_has_no_budget' => 'The transaction must have no budget', + 'search_modifier_not_has_any_budget' => 'The transaction must have no budget', + 'search_modifier_has_any_budget' => 'The transaction must have a (any) budget', + 'search_modifier_not_has_no_budget' => 'The transaction must have a (any) budget', + 'search_modifier_has_no_bill' => 'The transaction must have no bill', + 'search_modifier_not_has_no_bill' => 'The transaction must have a (any) bill', + 'search_modifier_has_any_bill' => 'The transaction must have a (any) bill', + 'search_modifier_not_has_any_bill' => 'The transaction must have no bill', + 'search_modifier_has_no_tag' => 'The transaction must have no tags', + 'search_modifier_not_has_any_tag' => 'The transaction must have no tags', + 'search_modifier_not_has_no_tag' => 'The transaction must have a (any) tag', + 'search_modifier_has_any_tag' => 'The transaction must have a (any) tag', + 'search_modifier_notes_contains' => 'The transaction notes contain ":value"', + 'search_modifier_not_notes_contains' => 'The transaction notes do not contain ":value"', + 'search_modifier_notes_starts' => 'The transaction notes start with ":value"', + 'search_modifier_not_notes_starts' => 'The transaction notes do not start with ":value"', + 'search_modifier_notes_ends' => 'The transaction notes end with ":value"', + 'search_modifier_not_notes_ends' => 'The transaction notes do not end with ":value"', + 'search_modifier_notes_is' => 'The transaction notes are exactly ":value"', + 'search_modifier_not_notes_is' => 'The transaction notes are exactly not ":value"', + 'search_modifier_no_notes' => 'The transaction has no notes', + 'search_modifier_not_no_notes' => 'The transaction must have notes', + 'search_modifier_any_notes' => 'The transaction must have notes', + 'search_modifier_not_any_notes' => 'The transaction has no notes', + 'search_modifier_amount_is' => 'Amount is exactly :value', + 'search_modifier_not_amount_is' => 'Amount is not :value', + 'search_modifier_amount_less' => 'Amount is less than or equal to :value', + 'search_modifier_not_amount_more' => 'Amount is less than or equal to :value', + 'search_modifier_amount_more' => 'Amount is more than or equal to :value', + 'search_modifier_not_amount_less' => 'Amount is more than or equal to :value', + 'search_modifier_source_account_is' => 'Source account name is exactly ":value"', + 'search_modifier_not_source_account_is' => 'Source account name is not ":value"', + 'search_modifier_source_account_contains' => 'Source account name contains ":value"', + 'search_modifier_not_source_account_contains' => 'Source account name does not contain ":value"', + 'search_modifier_source_account_starts' => 'Source account name starts with ":value"', + 'search_modifier_not_source_account_starts' => 'Source account name does not start with ":value"', + 'search_modifier_source_account_ends' => 'Source account name ends with ":value"', + 'search_modifier_not_source_account_ends' => 'Source account name does not end with ":value"', + 'search_modifier_source_account_id' => 'Source account ID is :value', + 'search_modifier_not_source_account_id' => 'Source account ID is not :value', + '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_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"', + 'search_modifier_not_source_account_nr_ends' => 'Source account number (IBAN) does not end on ":value"', + 'search_modifier_destination_account_is' => 'Destination account name is exactly ":value"', + 'search_modifier_not_destination_account_is' => 'Destination account name is not ":value"', + 'search_modifier_destination_account_contains' => 'Destination account name contains ":value"', + 'search_modifier_not_destination_account_contains' => 'Destination account name does not contain ":value"', + 'search_modifier_destination_account_starts' => 'Destination account name starts with ":value"', + 'search_modifier_not_destination_account_starts' => 'Destination account name does not start with ":value"', + 'search_modifier_destination_account_ends' => 'Destination account name ends on ":value"', + 'search_modifier_not_destination_account_ends' => 'Destination account name does not end on ":value"', + 'search_modifier_destination_account_id' => 'Destination account ID is :value', + 'search_modifier_not_destination_account_id' => 'Destination account ID is not :value', + 'search_modifier_destination_is_cash' => 'Destination account is the "(cash)" account', + 'search_modifier_not_destination_is_cash' => 'Destination account is not the "(cash)" account', + '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_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"', + 'search_modifier_not_destination_account_nr_starts' => 'Destination account number (IBAN) does not start with ":value"', + 'search_modifier_destination_account_nr_ends' => 'Destination account number (IBAN) ends with ":value"', + 'search_modifier_not_destination_account_nr_ends' => 'Destination account number (IBAN) does not end with ":value"', + 'search_modifier_account_id' => 'Source or destination account ID\'s is/are: :value', + 'search_modifier_not_account_id' => 'Source or destination account ID\'s is/are not: :value', + 'search_modifier_category_is' => 'Category is ":value"', + 'search_modifier_not_category_is' => 'Category is not ":value"', + 'search_modifier_budget_is' => 'Budget is ":value"', + 'search_modifier_not_budget_is' => 'Budget is not ":value"', + '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_tag_is' => 'Tag is ":value"', + 'search_modifier_not_tag_is' => 'No tag is ":value"', + 'search_modifier_date_on_year' => 'Transaction is in year ":value"', + 'search_modifier_not_date_on_year' => 'Transaction is not in year ":value"', + 'search_modifier_date_on_month' => 'Transaction is in month ":value"', + 'search_modifier_not_date_on_month' => 'Transaction is not in month ":value"', + 'search_modifier_date_on_day' => 'Transaction is on day of month ":value"', + 'search_modifier_not_date_on_day' => 'Transaction is not on day of month ":value"', + 'search_modifier_date_before_year' => 'Transaction is before or in year ":value"', + 'search_modifier_date_before_month' => 'Transaction is before or in month ":value"', + 'search_modifier_date_before_day' => 'Transaction is before or on day of month ":value"', + 'search_modifier_date_after_year' => 'Transaction is in or after year ":value"', + 'search_modifier_date_after_month' => 'Transaction is in or after month ":value"', + 'search_modifier_date_after_day' => 'Transaction is after or on day of month ":value"', // new - 'search_modifier_tag_is_not' => 'No tag is ":value"', - + 'search_modifier_tag_is_not' => 'No tag is ":value"', + 'search_modifier_not_tag_is_not' => 'Tag is ":value"', 'search_modifier_account_is' => 'Either account is ":value"', + 'search_modifier_not_account_is' => 'Neither account is ":value"', 'search_modifier_account_contains' => 'Either account contains ":value"', + 'search_modifier_not_account_contains' => 'Neither account contains ":value"', 'search_modifier_account_ends' => 'Either account ends with ":value"', + 'search_modifier_not_account_ends' => 'Neither account ends with ":value"', 'search_modifier_account_starts' => 'Either account starts with ":value"', + 'search_modifier_not_account_starts' => 'Neither account starts with ":value"', 'search_modifier_account_nr_is' => 'Either account number / IBAN is ":value"', + 'search_modifier_not_account_nr_is' => 'Neither account number / IBAN is ":value"', 'search_modifier_account_nr_contains' => 'Either account number / IBAN contains ":value"', + 'search_modifier_not_account_nr_contains' => 'Neither account number / IBAN contains ":value"', 'search_modifier_account_nr_ends' => 'Either account number / IBAN ends with ":value"', + 'search_modifier_not_account_nr_ends' => 'Neither account number / IBAN ends with ":value"', 'search_modifier_account_nr_starts' => 'Either account number / IBAN starts with ":value"', + 'search_modifier_not_account_nr_starts' => 'Neither account number / IBAN starts with ":value"', 'search_modifier_category_contains' => 'Category contains ":value"', 'search_modifier_not_category_contains' => 'Category does not contain ":value"', - 'search_modifier_category_ends' => 'Category ends with ":value"', + '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_budget_contains' => 'Budget contains ":value"', + 'search_modifier_not_budget_contains' => 'Budget does not contain ":value"', 'search_modifier_budget_ends' => 'Budget ends with ":value"', + 'search_modifier_not_budget_ends' => 'Budget does not end on ":value"', 'search_modifier_budget_starts' => 'Budget starts with ":value"', + 'search_modifier_not_budget_starts' => 'Budget does not end on ":value"', 'search_modifier_bill_contains' => 'Bill contains ":value"', + 'search_modifier_not_bill_contains' => 'Bill does not contain ":value"', 'search_modifier_bill_ends' => 'Bill ends with ":value"', + 'search_modifier_not_bill_ends' => 'Bill ends does not end on ":value"', '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_external_id_ends' => 'External ID ends with ":value"', 'search_modifier_external_id_starts' => 'External ID starts with ":value"', @@ -424,18 +492,29 @@ return [ 'search_modifier_external_url_ends' => 'External URL ends with ":value"', 'search_modifier_external_url_starts' => 'External URL starts with ":value"', 'search_modifier_has_no_attachments' => 'Transaction has no attachments', - 'search_modifier_account_is_cash' => 'Either account is a cash account.', + 'search_modifier_not_has_no_attachments' => 'Transaction has attachments', + 'search_modifier_not_has_attachments' => 'Transaction has no attachments', + 'search_modifier_account_is_cash' => 'Either account is the "(cash)" account.', + 'search_modifier_not_account_is_cash' => 'Neither account is the "(cash)" account.', '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_foreign_amount_is' => 'The foreign amount is ":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)', // date fields 'search_modifier_interest_date_on' => 'Transaction interest date is ":value"', + 'search_modifier_not_interest_date_on' => 'Transaction interest date is not ":value"', 'search_modifier_interest_date_on_year' => 'Transaction interest date is in year ":value"', + 'search_modifier_not_interest_date_on_year' => 'Transaction interest date is not in year ":value"', 'search_modifier_interest_date_on_month' => 'Transaction interest date is in month ":value"', + 'search_modifier_not_interest_date_on_month' => 'Transaction interest date is not in month ":value"', 'search_modifier_interest_date_on_day' => 'Transaction interest date is on day of month ":value"', + 'search_modifier_not_interest_date_on_day' => 'Transaction interest date is not on day of month ":value"', 'search_modifier_interest_date_before_year' => 'Transaction interest date is before or in year ":value"', 'search_modifier_interest_date_before_month' => 'Transaction interest date is before or in month ":value"', 'search_modifier_interest_date_before_day' => 'Transaction interest date is before or on day of month ":value"', @@ -445,6 +524,9 @@ return [ 'search_modifier_book_date_on_year' => 'Transaction book date is in year ":value"', 'search_modifier_book_date_on_month' => 'Transaction book date is in month ":value"', 'search_modifier_book_date_on_day' => 'Transaction book date is on day of month ":value"', + 'search_modifier_not_book_date_on_year' => 'Transaction book date is not in year ":value"', + 'search_modifier_not_book_date_on_month' => 'Transaction book date is not in month ":value"', + 'search_modifier_not_book_date_on_day' => 'Transaction book date is not on day of month ":value"', 'search_modifier_book_date_before_year' => 'Transaction book date is before or in year ":value"', 'search_modifier_book_date_before_month' => 'Transaction book date is before or in month ":value"', 'search_modifier_book_date_before_day' => 'Transaction book date is before or on day of month ":value"', @@ -454,6 +536,9 @@ return [ 'search_modifier_process_date_on_year' => 'Transaction process date is in year ":value"', 'search_modifier_process_date_on_month' => 'Transaction process date is in month ":value"', 'search_modifier_process_date_on_day' => 'Transaction process date is on day of month ":value"', + 'search_modifier_not_process_date_on_year' => 'Transaction process date is not in year ":value"', + 'search_modifier_not_process_date_on_month' => 'Transaction process date is not in month ":value"', + 'search_modifier_not_process_date_on_day' => 'Transaction process date is not on day of month ":value"', 'search_modifier_process_date_before_year' => 'Transaction process date is before or in year ":value"', 'search_modifier_process_date_before_month' => 'Transaction process date is before or in month ":value"', 'search_modifier_process_date_before_day' => 'Transaction process date is before or on day of month ":value"', @@ -463,6 +548,9 @@ return [ 'search_modifier_due_date_on_year' => 'Transaction due date is in year ":value"', 'search_modifier_due_date_on_month' => 'Transaction due date is in month ":value"', 'search_modifier_due_date_on_day' => 'Transaction due date is on day of month ":value"', + 'search_modifier_not_due_date_on_year' => 'Transaction due date is not in year ":value"', + 'search_modifier_not_due_date_on_month' => 'Transaction due date is not in month ":value"', + 'search_modifier_not_due_date_on_day' => 'Transaction due date is not on day of month ":value"', 'search_modifier_due_date_before_year' => 'Transaction due date is before or in year ":value"', 'search_modifier_due_date_before_month' => 'Transaction due date is before or in month ":value"', 'search_modifier_due_date_before_day' => 'Transaction due date is before or on day of month ":value"', @@ -472,6 +560,9 @@ return [ 'search_modifier_payment_date_on_year' => 'Transaction payment date is in year ":value"', 'search_modifier_payment_date_on_month' => 'Transaction payment date is in month ":value"', 'search_modifier_payment_date_on_day' => 'Transaction payment date is on day of month ":value"', + 'search_modifier_not_payment_date_on_year' => 'Transaction payment date is not in year ":value"', + 'search_modifier_not_payment_date_on_month' => 'Transaction payment date is not in month ":value"', + 'search_modifier_not_payment_date_on_day' => 'Transaction payment date is not on day of month ":value"', 'search_modifier_payment_date_before_year' => 'Transaction payment date is before or in year ":value"', 'search_modifier_payment_date_before_month' => 'Transaction payment date is before or in month ":value"', 'search_modifier_payment_date_before_day' => 'Transaction payment date is before or on day of month ":value"', @@ -481,6 +572,9 @@ return [ 'search_modifier_invoice_date_on_year' => 'Transaction invoice date is in year ":value"', 'search_modifier_invoice_date_on_month' => 'Transaction invoice date is in month ":value"', 'search_modifier_invoice_date_on_day' => 'Transaction invoice date is on day of month ":value"', + 'search_modifier_not_invoice_date_on_year' => 'Transaction invoice date is not in year ":value"', + 'search_modifier_not_invoice_date_on_month' => 'Transaction invoice date is not in month ":value"', + 'search_modifier_not_invoice_date_on_day' => 'Transaction invoice date is not on day of month ":value"', 'search_modifier_invoice_date_before_year' => 'Transaction invoice date is before or in year ":value"', 'search_modifier_invoice_date_before_month' => 'Transaction invoice date is before or in month ":value"', 'search_modifier_invoice_date_before_day' => 'Transaction invoice date is before or on day of month ":value"', @@ -491,6 +585,9 @@ return [ 'search_modifier_updated_at_on_year' => 'Transaction was last updated in year ":value"', 'search_modifier_updated_at_on_month' => 'Transaction was last updated in month ":value"', 'search_modifier_updated_at_on_day' => 'Transaction was last updated on day of month ":value"', + 'search_modifier_not_updated_at_on_year' => 'Transaction was not last updated in year ":value"', + 'search_modifier_not_updated_at_on_month' => 'Transaction was not last updated in month ":value"', + 'search_modifier_not_updated_at_on_day' => 'Transaction was not last updated on day of month ":value"', 'search_modifier_updated_at_before_year' => 'Transaction was last updated in or before year ":value"', 'search_modifier_updated_at_before_month' => 'Transaction was last updated in or before month ":value"', 'search_modifier_updated_at_before_day' => 'Transaction was last updated on or before day of month ":value"', @@ -500,6 +597,9 @@ return [ 'search_modifier_created_at_on_year' => 'Transaction was created in year ":value"', 'search_modifier_created_at_on_month' => 'Transaction was created in month ":value"', 'search_modifier_created_at_on_day' => 'Transaction was created on day of month ":value"', + 'search_modifier_not_created_at_on_year' => 'Transaction was not created in year ":value"', + 'search_modifier_not_created_at_on_month' => 'Transaction was not created in month ":value"', + 'search_modifier_not_created_at_on_day' => 'Transaction was not created on day of month ":value"', 'search_modifier_created_at_before_year' => 'Transaction was created in or before year ":value"', 'search_modifier_created_at_before_month' => 'Transaction was created in or before month ":value"', 'search_modifier_created_at_before_day' => 'Transaction was created on or before day of month ":value"', @@ -509,26 +609,34 @@ return [ 'search_modifier_interest_date_before' => 'Transaction interest date is on or before ":value"', 'search_modifier_interest_date_after' => 'Transaction interest date is on or after ":value"', 'search_modifier_book_date_on' => 'Transaction book date is on ":value"', + 'search_modifier_not_book_date_on' => 'Transaction book date is not on ":value"', 'search_modifier_book_date_before' => 'Transaction book date is on or before ":value"', 'search_modifier_book_date_after' => 'Transaction book date is on or after ":value"', 'search_modifier_process_date_on' => 'Transaction process date is on ":value"', + 'search_modifier_not_process_date_on' => 'Transaction process date is not on ":value"', 'search_modifier_process_date_before' => 'Transaction process date is on or before ":value"', 'search_modifier_process_date_after' => 'Transaction process date is on or after ":value"', 'search_modifier_due_date_on' => 'Transaction due date is on ":value"', + 'search_modifier_not_due_date_on' => 'Transaction due date is not on ":value"', 'search_modifier_due_date_before' => 'Transaction due date is on or before ":value"', 'search_modifier_due_date_after' => 'Transaction due date is on or after ":value"', 'search_modifier_payment_date_on' => 'Transaction payment date is on ":value"', + 'search_modifier_not_payment_date_on' => 'Transaction payment date is not on ":value"', 'search_modifier_payment_date_before' => 'Transaction payment date is on or before ":value"', 'search_modifier_payment_date_after' => 'Transaction payment date is on or after ":value"', 'search_modifier_invoice_date_on' => 'Transaction invoice date is on ":value"', + 'search_modifier_not_invoice_date_on' => 'Transaction invoice date is not on ":value"', 'search_modifier_invoice_date_before' => 'Transaction invoice date is on or before ":value"', 'search_modifier_invoice_date_after' => 'Transaction invoice date is on or after ":value"', 'search_modifier_created_at_on' => 'Transaction was created on ":value"', + 'search_modifier_not_created_at_on' => 'Transaction was not created on ":value"', 'search_modifier_created_at_before' => 'Transaction was created on or before ":value"', 'search_modifier_created_at_after' => 'Transaction was created on or after ":value"', 'search_modifier_updated_at_on' => 'Transaction was updated on ":value"', + 'search_modifier_not_updated_at_on' => 'Transaction was not updated on ":value"', 'search_modifier_updated_at_before' => 'Transaction was updated on or before ":value"', 'search_modifier_updated_at_after' => 'Transaction was updated on or after ":value"', + 'search_modifier_attachment_name_is' => 'Any attachment\'s name is ":value"', 'search_modifier_attachment_name_contains' => 'Any attachment\'s name contains ":value"', 'search_modifier_attachment_name_starts' => 'Any attachment\'s name starts with ":value"',