Improve test coverage and efficiency for accounts and budgets.

This commit is contained in:
James Cole
2019-06-23 11:13:36 +02:00
parent 8f25562923
commit 43d753e5bd
47 changed files with 919 additions and 985 deletions

View File

@@ -29,9 +29,7 @@ use FireflyIII\Models\Account;
use FireflyIII\Models\Category;
use FireflyIII\Models\Tag;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
@@ -44,15 +42,26 @@ use Log;
*
* TODO verify this all works as expected.
*
* - Always request start date and end date.
* - Group expenses, income, etc. under this period.
* - Returns collection of arrays. Possible fields are:
* - start (string),
* end (string),
* - Returns collection of arrays. Fields
* title (string),
* spent (string),
* earned (string),
* transferred (string)
* route (string)
* total_transactions (int)
* spent (array),
* earned (array),
* transferred_away (array)
* transferred_in (array)
*
* each array has the following format:
* currency_id => [
* currency_id : 1, (int)
* currency_symbol : X (str)
* currency_name: Euro (str)
* currency_code: EUR (str)
* amount: -1234 (str)
* count: 23
* ]
*
*/
trait PeriodOverview
@@ -63,20 +72,15 @@ trait PeriodOverview
* and for each period, the amount of money spent and earned. This is a complex operation which is cached for
* performance reasons.
*
* The method has been refactored recently for better performance.
*
* @param Account $account The account involved
* @param Carbon $date The start date.
* @param Carbon $end The end date.
*
* @return Collection
* @return array
*/
protected function getAccountPeriodOverview(Account $account, Carbon $date): Collection
protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$range = app('preferences')->get('viewRange', '1M')->data;
$end = $repository->oldestJournalDate($account) ?? Carbon::now()->subMonth()->startOfMonth();
$start = clone $date;
$range = app('preferences')->get('viewRange', '1M')->data;
if ($end < $start) {
[$start, $end] = [$end, $start]; // @codeCoverageIgnore
@@ -93,43 +97,54 @@ trait PeriodOverview
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
$entries = [];
// collect all expenses in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setAccounts(new Collection([$account]));
$collector->setRange($start, $end);
$collector->setTypes([TransactionType::DEPOSIT]);
$earnedSet = $collector->getExtractedJournals();
// collect all income in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setAccounts(new Collection([$account]));
$collector->setRange($start, $end);
$collector->setTypes([TransactionType::WITHDRAWAL]);
$spentSet = $collector->getExtractedJournals();
// collect all transfers in this period:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setAccounts(new Collection([$account]));
$collector->setRange($start, $end);
$collector->setTypes([TransactionType::TRANSFER]);
$transferSet = $collector->getExtractedJournals();
// loop dates
foreach ($dates as $currentDate) {
// collect from start to end:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setAccounts(new Collection([$account]));
$collector->setRange($currentDate['start'], $currentDate['end']);
$collector->setTypes([TransactionType::DEPOSIT]);
$earnedSet = $collector->getExtractedJournals();
$earned = $this->groupByCurrency($earnedSet);
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setAccounts(new Collection([$account]));
$collector->setRange($currentDate['start'], $currentDate['end']);
$collector->setTypes([TransactionType::WITHDRAWAL]);
$spentSet = $collector->getExtractedJournals();
$spent = $this->groupByCurrency($spentSet);
$title = app('navigation')->periodShow($currentDate['start'], $currentDate['period']);
/** @noinspection PhpUndefinedMethodInspection */
$entries->push(
[
'transactions' => count($spentSet) + count($earnedSet),
'title' => $title,
'spent' => $spent,
'earned' => $earned,
'transferred' => '0',
'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
]
);
}
//$cache->store($entries);
$earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
$spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
$transferredAway = $this->filterTransferredAway($account, $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']));
$transferredIn = $this->filterTransferredIn($account, $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']));
$entries[] =
[
'title' => $title,
'route' =>
route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'total_transactions' => count($spent) + count($earned) + count($transferredAway) + count($transferredIn),
'spent' => $this->groupByCurrency($spent),
'earned' => $this->groupByCurrency($earned),
'transferred_away' => $this->groupByCurrency($transferredAway),
'transferred_in' => $this->groupByCurrency($transferredIn),
];
}
$cache->store($entries);
return $entries;
}
@@ -144,6 +159,7 @@ trait PeriodOverview
*/
protected function getCategoryPeriodOverview(Category $category, Carbon $date): Collection
{
die('not yet complete');
/** @var JournalRepositoryInterface $journalRepository */
$journalRepository = app(JournalRepositoryInterface::class);
$range = app('preferences')->get('viewRange', '1M')->data;
@@ -208,17 +224,13 @@ trait PeriodOverview
*
* This method has been refactored recently.
*
* @param Carbon $start
* @param Carbon $date
*
* @return Collection
* @return array
*/
protected function getNoBudgetPeriodOverview(Carbon $date): Collection
protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array
{
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$first = $repository->firstNull();
$end = null === $first ? new Carbon : $first->date;
$start = clone $date;
$range = app('preferences')->get('viewRange', '1M')->data;
if ($end < $start) {
@@ -231,32 +243,33 @@ trait PeriodOverview
$cache->addProperty('no-budget-period-entries');
if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore
//return $cache->get(); // @codeCoverageIgnore
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
$entries = [];
// get all expenses without a budget.
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionType::WITHDRAWAL]);
$journals = $collector->getExtractedJournals();
foreach ($dates as $currentDate) {
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setRange($currentDate['start'], $currentDate['end'])->withoutBudget()->withAccountInformation()->setTypes(
[TransactionType::WITHDRAWAL]
);
$journals = $collector->getExtractedJournals();
$count = count($journals);
$spent = $this->groupByCurrency($journals);
$set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']);
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$entries->push(
$entries[] =
[
'transactions' => $count,
'title' => $title,
'spent' => $spent,
'earned' => '0',
'transferred' => '0',
'title' => $title,
'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
]
);
'total_transactions' => count($set),
'spent' => $this->groupByCurrency($set),
'earned' => [],
'transferred_away' => [],
'transferred_in' => [],
];
}
$cache->store($entries);
@@ -275,6 +288,7 @@ trait PeriodOverview
*/
protected function getNoCategoryPeriodOverview(Carbon $theDate): Collection // period overview method.
{
die('not yet complete');
Log::debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d')));
$range = app('preferences')->get('viewRange', '1M')->data;
$first = $this->journalRepos->firstNull();
@@ -361,6 +375,7 @@ trait PeriodOverview
*/
protected function getTagPeriodOverview(Tag $tag, Carbon $date): Collection // period overview for tags.
{
die('not yet complete');
/** @var TagRepositoryInterface $repository */
$repository = app(TagRepositoryInterface::class);
$range = app('preferences')->get('viewRange', '1M')->data;
@@ -423,6 +438,7 @@ trait PeriodOverview
*/
protected function getTransactionPeriodOverview(string $transactionType, Carbon $endDate): Collection
{
die('not yet complete');
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$range = app('preferences')->get('viewRange', '1M')->data;
@@ -519,6 +535,65 @@ trait PeriodOverview
return $return;
}
/**
* Return only transactions where $account is the source.
* @param Account $account
* @param array $journals
* @return array
*/
private function filterTransferredAway(Account $account, array $journals): array
{
$return = [];
/** @var array $journal */
foreach ($journals as $journal) {
if ($account->id === (int)$journal['source_account_id']) {
$return[] = $journal;
}
}
return $return;
}
/**
* Return only transactions where $account is the source.
* @param Account $account
* @param array $journals
* @return array
*/
private function filterTransferredIn(Account $account, array $journals): array
{
$return = [];
/** @var array $journal */
foreach ($journals as $journal) {
if ($account->id === (int)$journal['destination_account_id']) {
$return[] = $journal;
}
}
return $return;
}
/**
* Filter a list of journals by a set of dates, and then group them by currency.
*
* @param array $array
* @param Carbon $start
* @param Carbon $end
* @return array
*/
private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array
{
$result = [];
/** @var array $journal */
foreach ($array as $journal) {
if ($journal['date'] <= $end && $journal['date'] >= $start) {
$result[] = $journal;
}
}
return $result;
}
/**
* @param array $journals
*
@@ -529,19 +604,40 @@ trait PeriodOverview
$return = [];
/** @var array $journal */
foreach ($journals as $journal) {
$currencyId = (int)$journal['currency_id'];
$currencyId = (int)$journal['currency_id'];
$foreignCurrencyId = $journal['foreign_currency_id'];
if (!isset($return[$currencyId])) {
$currency = new TransactionCurrency;
$currency->symbol = $journal['currency_symbol'];
$currency->decimal_places = $journal['currency_decimal_places'];
$currency->name = $journal['currency_name'];
$return[$currencyId] = [
'amount' => '0',
'currency' => $currency,
//'currency' => 'x',//$currency,
$return[$currencyId] = [
'amount' => '0',
'count' => 0,
'currency_id' => $currencyId,
'currency_name' => $journal['currency_name'],
'currency_code' => $journal['currency_code'],
'currency_symbol' => $journal['currency_symbol'],
'currency_decimal_places' => $journal['currency_decimal_places'],
];
}
$return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $journal['amount']);
$return[$currencyId]['count']++;
if (null !== $foreignCurrencyId) {
if (!isset($return[$foreignCurrencyId])) {
$return[$foreignCurrencyId] = [
'amount' => '0',
'count' => 0,
'currency_id' => (int)$foreignCurrencyId,
'currency_name' => $journal['foreign_currency_name'],
'currency_code' => $journal['foreign_currency_code'],
'currency_symbol' => $journal['foreign_currency_symbol'],
'currency_decimal_places' => $journal['foreign_currency_decimal_places'],
];
}
$return[$foreignCurrencyId]['count']++;
$return[$foreignCurrencyId]['amount'] = bcadd($return[$foreignCurrencyId]['amount'], $journal['foreign_amount']);
}
}
return $return;

View File

@@ -25,6 +25,8 @@ namespace FireflyIII\Support\Twig\Extension;
use Carbon\Carbon;
use DB;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use Log;
use Twig_Extension;
@@ -43,7 +45,8 @@ class TransactionGroupTwig extends Twig_Extension
public function getFunctions(): array
{
return [
$this->transactionAmount(),
$this->journalArrayAmount(),
$this->journalObjectAmount(),
$this->groupAmount(),
$this->journalHasMeta(),
$this->journalGetMetaDate(),
@@ -160,18 +163,44 @@ class TransactionGroupTwig extends Twig_Extension
}
/**
* Shows the amount for a single journal array.
*
* @return Twig_SimpleFunction
*/
public function transactionAmount(): Twig_SimpleFunction
public function journalArrayAmount(): Twig_SimpleFunction
{
return new Twig_SimpleFunction(
'transactionAmount',
'journalArrayAmount',
function (array $array): string {
// if is not a withdrawal, amount positive.
$result = $this->normalAmount($array);
$result = $this->normalJournalArrayAmount($array);
// now append foreign amount, if any.
if (null !== $array['foreign_amount']) {
$foreign = $this->foreignAmount($array);
$foreign = $this->foreignJournalArrayAmount($array);
$result = sprintf('%s (%s)', $result, $foreign);
}
return $result;
},
['is_safe' => ['html']]
);
}
/**
* Shows the amount for a single journal object.
*
* @return Twig_SimpleFunction
*/
public function journalObjectAmount(): Twig_SimpleFunction
{
return new Twig_SimpleFunction(
'journalObjectAmount',
function (TransactionJournal $journal): string {
// if is not a withdrawal, amount positive.
$result = $this->normalJournalObjectAmount($journal);
// now append foreign amount, if any.
if ($this->journalObjectHasForeign($journal)) {
$foreign = $this->foreignJournalObjectAmount($journal);
$result = sprintf('%s (%s)', $result, $foreign);
}
@@ -188,7 +217,7 @@ class TransactionGroupTwig extends Twig_Extension
*
* @return string
*/
private function foreignAmount(array $array): string
private function foreignJournalArrayAmount(array $array): string
{
$type = $array['transaction_type_type'] ?? TransactionType::WITHDRAWAL;
$amount = $array['foreign_amount'] ?? '0';
@@ -207,6 +236,35 @@ class TransactionGroupTwig extends Twig_Extension
return $result;
}
/**
* Generate foreign amount for journal from a transaction group.
*
* @param TransactionJournal $journal
*
* @return string
*/
private function foreignJournalObjectAmount(TransactionJournal $journal): string
{
$type = $journal->transactionType->type;
/** @var Transaction $first */
$first = $journal->transactions()->where('amount', '<', 0)->first();
$currency = $first->foreignCurrency;
$amount = $first->foreign_amount ?? '0';
$colored = true;
if ($type !== TransactionType::WITHDRAWAL) {
$amount = bcmul($amount, '-1');
}
if ($type === TransactionType::TRANSFER) {
$colored = false;
}
$result = app('amount')->formatFlat($currency->symbol, (int)$currency->decimal_places, $amount, $colored);
if ($type === TransactionType::TRANSFER) {
$result = sprintf('<span class="text-info">%s</span>', $result);
}
return $result;
}
/**
* Generate normal amount for transaction from a transaction group.
*
@@ -214,7 +272,7 @@ class TransactionGroupTwig extends Twig_Extension
*
* @return string
*/
private function normalAmount(array $array): string
private function normalJournalArrayAmount(array $array): string
{
$type = $array['transaction_type_type'] ?? TransactionType::WITHDRAWAL;
$amount = $array['amount'] ?? '0';
@@ -232,4 +290,44 @@ class TransactionGroupTwig extends Twig_Extension
return $result;
}
/**
* Generate normal amount for transaction from a transaction group.
*
* @param TransactionJournal $journal
*
* @return string
*/
private function normalJournalObjectAmount(TransactionJournal $journal): string
{
$type = $journal->transactionType->type;
$first = $journal->transactions()->where('amount', '<', 0)->first();
$currency = $journal->transactionCurrency;
$amount = $first->amount ?? '0';
$colored = true;
if ($type !== TransactionType::WITHDRAWAL) {
$amount = bcmul($amount, '-1');
}
if ($type === TransactionType::TRANSFER) {
$colored = false;
}
$result = app('amount')->formatFlat($currency->symbol, (int)$currency->decimal_places, $amount, $colored);
if ($type === TransactionType::TRANSFER) {
$result = sprintf('<span class="text-info">%s</span>', $result);
}
return $result;
}
/**
* @param TransactionJournal $journal
* @return bool
*/
private function journalObjectHasForeign(TransactionJournal $journal): bool
{
/** @var Transaction $first */
$first = $journal->transactions()->where('amount', '<', 0)->first();
return null !== $first->foreign_amount;
}
}

View File

@@ -48,4 +48,34 @@ class Translation extends Twig_Extension
return $filters;
}
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
$this->journalLinkTranslation(),
];
}
/**
* @return Twig_SimpleFunction
*/
public function journalLinkTranslation(): Twig_SimpleFunction
{
return new Twig_SimpleFunction(
'journalLinkTranslation',
function (string $direction, string $original) {
$key = sprintf('firefly.%s_%s', $original, $direction);
$translation = trans($key);
if ($key === $translation) {
return $original;
}
return $translation;
},
['is_safe' => ['html']]
);
}
}