From c54da6200599d26b842a263457c8ba74d7b7359e Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 27 Sep 2025 05:31:26 +0200 Subject: [PATCH] Enable period statistics for category. --- app/Models/Category.php | 5 + .../Category/CategoryRepository.php | 39 ++++++ .../Category/CategoryRepositoryInterface.php | 2 + .../Http/Controllers/PeriodOverview.php | 129 +++++++++++++++--- 4 files changed, 158 insertions(+), 17 deletions(-) diff --git a/app/Models/Category.php b/app/Models/Category.php index 018fbdbef4..55c9c7ebcf 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -109,4 +109,9 @@ class Category extends Model 'user_group_id' => 'integer', ]; } + + public function primaryPeriodStatistics(): MorphMany + { + return $this->morphMany(PeriodStatistic::class, 'primary_statable'); + } } diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index 487a467c20..f644c57070 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -358,4 +358,43 @@ class CategoryRepository implements CategoryRepositoryInterface, UserGroupInterf return $service->update($category, $data); } + + public function periodCollection(Category $category, Carbon $start, Carbon $end): array + { + Log::debug(sprintf('periodCollection(#%d, %s, %s)', $category->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); + + return $category->transactionJournals() + ->leftJoin('transactions','transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id') + ->where('transaction_journals.date', '>=', $start) + ->where('transaction_journals.date', '<=', $end) + ->where('transactions.amount', '>', 0) + ->get([ + // currencies + 'transaction_currencies.id as currency_id', + 'transaction_currencies.code as currency_code', + 'transaction_currencies.name as currency_name', + 'transaction_currencies.symbol as currency_symbol', + 'transaction_currencies.decimal_places as currency_decimal_places', + + // foreign + 'foreign_currencies.id as foreign_currency_id', + 'foreign_currencies.code as foreign_currency_code', + 'foreign_currencies.name as foreign_currency_name', + 'foreign_currencies.symbol as foreign_currency_symbol', + 'foreign_currencies.decimal_places as foreign_currency_decimal_places', + + // fields + 'transaction_journals.date', + 'transaction_types.type', + 'transaction_journals.transaction_currency_id', + 'transactions.amount', + 'transactions.native_amount as pc_amount', + 'transactions.foreign_amount', + ]) + ->toArray() + ; + } } diff --git a/app/Repositories/Category/CategoryRepositoryInterface.php b/app/Repositories/Category/CategoryRepositoryInterface.php index 263c11c716..cef58d2d17 100644 --- a/app/Repositories/Category/CategoryRepositoryInterface.php +++ b/app/Repositories/Category/CategoryRepositoryInterface.php @@ -48,6 +48,8 @@ interface CategoryRepositoryInterface public function categoryStartsWith(string $query, int $limit): Collection; + public function periodCollection(Category $category, Carbon $start, Carbon $end): array; + public function destroy(Category $category): bool; /** diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php index 4081faba0c..1995bc15c8 100644 --- a/app/Support/Http/Controllers/PeriodOverview.php +++ b/app/Support/Http/Controllers/PeriodOverview.php @@ -27,17 +27,20 @@ namespace FireflyIII\Support\Http\Controllers; use Carbon\Carbon; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Generator\Report\Category\YearReportGenerator; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\Category; use FireflyIII\Models\PeriodStatistic; use FireflyIII\Models\Tag; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface; use FireflyIII\Support\CacheProperties; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Navigation; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; @@ -71,6 +74,7 @@ use Illuminate\Support\Facades\Log; trait PeriodOverview { protected AccountRepositoryInterface $accountRepository; + protected CategoryRepositoryInterface $categoryRepository; protected JournalRepositoryInterface $journalRepos; protected PeriodStatisticRepositoryInterface $periodStatisticRepo; private Collection $statistics; // temp data holder @@ -87,6 +91,7 @@ trait PeriodOverview { Log::debug(sprintf('Now in getAccountPeriodOverview(#%d, %s %s)', $account->id, $start->format('Y-m-d H:i:s.u'), $end->format('Y-m-d H:i:s.u'))); $this->accountRepository = app(AccountRepositoryInterface::class); + $this->accountRepository->setUser($account->user); $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; @@ -138,24 +143,27 @@ trait PeriodOverview */ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array { + $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->categoryRepository->setUser($category->user); + $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class); + $range = Navigation::getViewRange(true); [$start, $end] = $end < $start ? [$end, $start] : [$start, $end]; - // properties for entries with their amounts. - $cache = new CacheProperties(); - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty($range); - $cache->addProperty('category-show-period-entries'); - $cache->addProperty($category->id); - - if ($cache->has()) { - return $cache->get(); - } - /** @var array $dates */ $dates = Navigation::blockPeriods($start, $end, $range); $entries = []; + [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end); + $this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end); + + + Log::debug(sprintf('Count of loops: %d', count($dates))); + foreach ($dates as $currentDate) { + $entries[] = $this->getSingleCategoryPeriod($category, $currentDate['period'], $currentDate['start'], $currentDate['end']); + } + + return $entries; + // collect all expenses in this period: /** @var GroupCollectorInterface $collector */ @@ -199,7 +207,6 @@ trait PeriodOverview 'transferred' => $this->groupByCurrency($transferred), ]; } - $cache->store($entries); return $entries; } @@ -324,6 +331,7 @@ trait PeriodOverview return $entries; } + protected function getSingleAccountPeriod(Account $account, string $period, Carbon $start, Carbon $end): array { Log::debug(sprintf('Now in getSingleAccountPeriod(#%d, %s %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); @@ -344,6 +352,26 @@ trait PeriodOverview return $return; } + protected function getSingleCategoryPeriod(Category $category, string $period, Carbon $start, Carbon $end): array + { + Log::debug(sprintf('Now in getSingleCategoryPeriod(#%d, %s %s)', $category->id, $start->format('Y-m-d'), $end->format('Y-m-d'))); + $types = ['spent', 'earned', 'transferred_in', 'transferred_away']; + $return = [ + 'title' => Navigation::periodShow($start, $period), + 'route' => route('categories.show', [$category->id, $start->format('Y-m-d'), $end->format('Y-m-d')]), + 'total_transactions' => 0, + ]; + $this->transactions = []; + foreach ($types as $type) { + $set = $this->getSingleCategoryPeriodByType($category, $start, $end, $type); + $return['total_transactions'] += $set['count']; + unset($set['count']); + $return[$type] = $set; + } + + return $return; + } + protected function filterStatistics(Carbon $start, Carbon $end, string $type): Collection { return $this->statistics->filter( @@ -432,6 +460,73 @@ trait PeriodOverview return $grouped; } + protected function getSingleCategoryPeriodByType(Category $category, Carbon $start, Carbon $end, string $type): array + { + Log::debug(sprintf('Now in getSingleCategoryPeriodByType(#%d, %s %s, %s)', $category->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type)); + $statistics = $this->filterStatistics($start, $end, $type); + + // nothing found, regenerate them. + if (0 === $statistics->count()) { + Log::debug(sprintf('Found nothing in this period for type "%s"', $type)); + if (0 === count($this->transactions)) { + $this->transactions = $this->categoryRepository->periodCollection($category, $start, $end); + } + + switch ($type) { + default: + throw new FireflyException(sprintf('Cannot deal with category period type %s', $type)); + + case 'spent': + $result = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $start, $end); + + break; + + case 'earned': + $result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $start, $end); + + break; + + case 'transferred_in': + $result = $this->filterTransfers('in', $start, $end); + + break; + + case 'transferred_away': + $result = $this->filterTransfers('away', $start, $end); + + break; + } + // each result must be grouped by currency, then saved as period statistic. + Log::debug(sprintf('Going to group %d found journal(s)', count($result))); + $grouped = $this->groupByCurrency($result); + + $this->saveGroupedAsStatistics($category, $start, $end, $type, $grouped); + + return $grouped; + } + $grouped = [ + 'count' => 0, + ]; + + /** @var PeriodStatistic $statistic */ + foreach ($statistics as $statistic) { + $id = (int)$statistic->transaction_currency_id; + $currency = Amount::getTransactionCurrencyById($id); + $grouped[$id] = [ + 'amount' => (string)$statistic->amount, + 'count' => (int)$statistic->count, + 'currency_id' => $currency->id, + 'currency_name' => $currency->name, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + ]; + $grouped['count'] += (int)$statistic->count; + } + + return $grouped; + } + /** * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums. * @@ -569,16 +664,16 @@ trait PeriodOverview return $entries; } - protected function saveGroupedAsStatistics(Account $account, Carbon $start, Carbon $end, string $type, array $array): void + protected function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void { unset($array['count']); - Log::debug(sprintf('saveGroupedAsStatistics(#%d, %s, %s, "%s", array(%d))', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); + Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))',get_class($model), $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array))); foreach ($array as $entry) { - $this->periodStatisticRepo->saveStatistic($account, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); + $this->periodStatisticRepo->saveStatistic($model, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']); } if (0 === count($array)) { Log::debug('Save empty statistic.'); - $this->periodStatisticRepo->saveStatistic($account, $this->primaryCurrency->id, $start, $end, $type, 0, '0'); + $this->periodStatisticRepo->saveStatistic($model, $this->primaryCurrency->id, $start, $end, $type, 0, '0'); } }