Budget box is multi-currency.

This commit is contained in:
James Cole 2019-08-16 08:27:08 +02:00
parent b4a732bf77
commit 070f46c755
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
5 changed files with 218 additions and 119 deletions

View File

@ -57,8 +57,8 @@ class BudgetReportHelper implements BudgetReportHelperInterface
/**
* Get the full budget report.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* TODO one big method is very complex.
*
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
@ -68,104 +68,142 @@ class BudgetReportHelper implements BudgetReportHelperInterface
public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts): array
{
$set = $this->repository->getBudgets();
$array = [];
$array = [
'budgets' => [],
'sums' => [],
];
/** @var Budget $budget */
foreach ($set as $budget) {
$entry = [
'budget_id' => $budget->id,
'budget_name' => $budget->name,
'no_budget' => false,
'rows' => [],
];
// get multi currency expenses first:
$budgetLimits = $this->repository->getBudgetLimits($budget, $start, $end);
if (0 === $budgetLimits->count()) { // no budget limit(s) for this budget
$spent = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end); // spent for budget in time range
if (bccomp($spent, '0') === -1) {
$line = [
'type' => 'budget',
'id' => $budget->id,
'name' => $budget->name,
'budgeted' => '0',
'spent' => $spent,
'left' => '0',
'overspent' => '0',
$expenses = $this->repository->spentInPeriodMc(new Collection([$budget]), $accounts, $start, $end);
if (0 === count($expenses)) {
// list the budget limits, basic amounts.
/** @var BudgetLimit $limit */
foreach ($budgetLimits as $limit) {
$row = [
'limit_id' => $limit->id,
'start_date' => $limit->start_date,
'end_date' => $limit->end_date,
'budgeted' => $limit->amount,
'spent' => '0',
'left' => $limit->amount,
'overspent' => null,
'currency_id' => $limit->transactionCurrency->id,
'currency_code' => $limit->transactionCurrency->code,
'currency_name' => $limit->transactionCurrency->name,
'currency_symbol' => $limit->transactionCurrency->symbol,
'currency_decimal_places' => $limit->transactionCurrency->decimal_places,
];
$array[] = $line;
$entry['rows'][] = $row;
}
continue;
}
/** @var BudgetLimit $budgetLimit */
foreach ($budgetLimits as $budgetLimit) { // one or more repetitions for budget
$data = $this->calculateExpenses($budget, $budgetLimit, $accounts);
$line = [
'type' => 'budget-line',
'start' => $budgetLimit->start_date,
'end' => $budgetLimit->end_date,
'limit' => $budgetLimit->id,
'id' => $budget->id,
'name' => $budget->name,
'budgeted' => (string)$budgetLimit->amount,
'spent' => $data['expenses'],
'left' => $data['left'],
'overspent' => $data['overspent'],
foreach ($expenses as $expense) {
$limit = $this->budgetLimitInCurrency($expense['currency_id'], $budgetLimits);
$row = [
'limit_id' => null,
'start_date' => null,
'end_date' => null,
'budgeted' => null,
'spent' => $expense['amount'],
'left' => null,
'overspent' => null,
'currency_id' => $expense['currency_id'],
'currency_code' => $expense['currency_name'],
'currency_name' => $expense['currency_name'],
'currency_symbol' => $expense['currency_symbol'],
'currency_decimal_places' => $expense['currency_decimal_places'],
];
$array[] = $line;
if (null !== $limit) {
// yes
$row['start_date'] = $limit->start_date;
$row['end_date'] = $limit->end_date;
$row['budgeted'] = $limit->amount;
$row['limit_id'] = $limit->id;
// less than zero? Set to 0.0
$row['left'] = -1 === bccomp(bcadd($limit->amount, $row['spent']), '0') ? '0' : bcadd($limit->amount, $row['spent']);
// spent > budgeted? then sum, otherwise other sum
$row['overspent'] = 1 === bccomp($row['spent'], $row['budgeted']) ? bcadd($row['spent'], $row['budgeted']) : '0';
}
$entry['rows'][] = $row;
}
$array['budgets'][] = $entry;
}
$noBudget = $this->repository->spentInPeriodWoBudget($accounts, $start, $end); // stuff outside of budgets
$line = [
'type' => 'no-budget',
'budgeted' => '0',
'spent' => $noBudget,
'left' => '0',
'overspent' => '0',
$noBudget = $this->repository->spentInPeriodWoBudgetMc($accounts, $start, $end);
$noBudgetEntry = [
'budget_id' => null,
'budget_name' => null,
'no_budget' => true,
'rows' => [],
];
$array[] = $line;
foreach ($noBudget as $row) {
$noBudgetEntry['rows'][] = [
'limit_id' => null,
'start_date' => null,
'end_date' => null,
'budgeted' => null,
'spent' => $row['amount'],
'left' => null,
'overspent' => null,
'currency_id' => $row['currency_id'],
'currency_code' => $row['currency_code'],
'currency_name' => $row['currency_name'],
'currency_symbol' => $row['currency_symbol'],
'currency_decimal_places' => $row['currency_decimal_places'],
];
}
$array['budgets'][] = $noBudgetEntry;
return $array;
}
/**
* Get all budgets and the expenses in these budgets.
*
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return Collection
*/
public function getBudgetsWithExpenses(Carbon $start, Carbon $end, Collection $accounts): Collection
{
/** @var BudgetRepositoryInterface $repository */
$repository = app(BudgetRepositoryInterface::class);
$budgets = $repository->getActiveBudgets();
$set = new Collection;
/** @var Budget $budget */
foreach ($budgets as $budget) {
$total = $repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end);
if (bccomp($total, '0') === -1) {
$set->push($budget);
// fill sums:
/** @var array $budget */
foreach ($array['budgets'] as $budget) {
/** @var array $row */
foreach ($budget['rows'] as $row) {
$currencyId = $row['currency_id'];
$array['sums'][$currencyId] = $array['sums'][$currencyId] ?? [
'currency_id' => $row['currency_id'],
'currency_code' => $row['currency_code'],
'currency_name' => $row['currency_name'],
'currency_symbol' => $row['currency_symbol'],
'currency_decimal_places' => $row['currency_decimal_places'],
'budgeted' => '0',
'spent' => '0',
'left' => '0',
'overspent' => '0',
];
$array['sums'][$currencyId]['budgeted'] = bcadd($array['sums'][$currencyId]['budgeted'], $row['budgeted'] ?? '0');
$array['sums'][$currencyId]['spent'] = bcadd($array['sums'][$currencyId]['spent'], $row['spent'] ?? '0');
$array['sums'][$currencyId]['left'] = bcadd($array['sums'][$currencyId]['left'], $row['left'] ?? '0');
$array['sums'][$currencyId]['overspent'] = bcadd($array['sums'][$currencyId]['overspent'], $row['overspent'] ?? '0');
}
}
return $set;
return $array;
}
/**
* Calculate the expenses for a budget.
* Returns from the collection the budget limit with the indicated currency ID
*
* @param Budget $budget
* @param BudgetLimit $budgetLimit
* @param Collection $accounts
* @param int $currencyId
* @param Collection $budgetLimits
*
* @return array
* @return BudgetLimit|null
*/
private function calculateExpenses(Budget $budget, BudgetLimit $budgetLimit, Collection $accounts): array
private function budgetLimitInCurrency(int $currencyId, Collection $budgetLimits): ?BudgetLimit
{
$array = [];
$expenses = $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $budgetLimit->start_date, $budgetLimit->end_date);
$array['left'] = 1 === bccomp(bcadd($budgetLimit->amount, $expenses), '0') ? bcadd($budgetLimit->amount, $expenses) : '0';
$array['spent'] = 1 === bccomp(bcadd($budgetLimit->amount, $expenses), '0') ? $expenses : '0';
$array['overspent'] = 1 === bccomp(bcadd($budgetLimit->amount, $expenses), '0') ? '0' : bcadd($expenses, $budgetLimit->amount);
$array['expenses'] = $expenses;
return $array;
return $budgetLimits->first(
static function (BudgetLimit $limit) use ($currencyId) {
return $limit->transaction_currency_id === $currencyId;
}
);
}
}

View File

@ -40,15 +40,4 @@ interface BudgetReportHelperInterface
* @return array
*/
public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts): array;
/**
* Get budgets and the expenses in each budget.
*
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return Collection
*/
public function getBudgetsWithExpenses(Carbon $start, Carbon $end, Collection $accounts): Collection;
}

View File

@ -57,17 +57,17 @@ class BudgetController extends Controller
$cache->addProperty('budget-report');
$cache->addProperty($accounts->pluck('id')->toArray());
if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore
//return $cache->get(); // @codeCoverageIgnore
}
$helper = app(BudgetReportHelperInterface::class);
$budgets = $helper->getBudgetReport($start, $end, $accounts);
try {
//try {
$result = view('reports.partials.budgets', compact('budgets'))->render();
// @codeCoverageIgnoreStart
} catch (Throwable $e) {
Log::debug(sprintf('Could not render reports.partials.budgets: %s', $e->getMessage()));
$result = 'Could not render view.';
}
// } catch (Throwable $e) {
// Log::debug(sprintf('Could not render reports.partials.budgets: %s', $e->getMessage()));
// $result = 'Could not render view.';
// }
// @codeCoverageIgnoreEnd
$cache->store($result);

View File

@ -179,10 +179,10 @@ class BudgetRepository implements BudgetRepositoryInterface
public function getBudgetLimits(Budget $budget, Carbon $start = null, Carbon $end = null): Collection
{
if (null === $end && null === $start) {
return $budget->budgetlimits()->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']);
return $budget->budgetlimits()->with(['transactionCurrency'])->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']);
}
if (null === $end xor null === $start) {
$query = $budget->budgetlimits()->orderBy('budget_limits.start_date', 'DESC');
$query = $budget->budgetlimits()->with(['transactionCurrency'])->orderBy('budget_limits.start_date', 'DESC');
// one of the two is null
if (null !== $end) {
// end date must be before $end.
@ -704,6 +704,7 @@ class BudgetRepository implements BudgetRepositoryInterface
'id' => $transaction['currency_id'],
'decimal_places' => $transaction['currency_decimal_places'],
'code' => $transaction['currency_code'],
'name' => $transaction['currency_name'],
'symbol' => $transaction['currency_symbol'],
];
}
@ -720,9 +721,10 @@ class BudgetRepository implements BudgetRepositoryInterface
$return[] = [
'currency_id' => $currency['id'],
'currency_code' => $code,
'currency_name' => $currency['name'],
'currency_symbol' => $currency['symbol'],
'currency_decimal_places' => $currency['decimal_places'],
'amount' => round($spent, $currency['decimal_places']),
'amount' => $spent,
];
}
@ -792,9 +794,10 @@ class BudgetRepository implements BudgetRepositoryInterface
$return[] = [
'currency_id' => $currency['id'],
'currency_code' => $code,
'currency_name' => $currency['name'],
'currency_symbol' => $currency['symbol'],
'currency_decimal_places' => $currency['decimal_places'],
'amount' => round($spent, $currency['decimal_places']),
'amount' => $spent,
];
}

View File

@ -11,19 +11,74 @@
</tr>
</thead>
<tbody>
{% set sum_budgeted = 0 %}
{% set sum_spent = 0 %}
{% set sum_left = 0 %}
{% set sum_overspent = 0 %}
{% for line in budgets %}
{% for budget in budgets.budgets %}
{% for row in budget.rows %}
<tr>
<!-- budget name, always visible -->
{% if budget.no_budget %}
<td data-value="zzz">
<em>{{ 'no_budget'|_ }} ({{ row.currency_name }})</em>
</td>
{% else %}
<td data-value="{{ budget.budget_name }}">
<a href="{{ route('budgets.show',budget.budget_id) }}">{{ budget.budget_name }}</a>
</td>
{% endif %}
<!-- date, hidden on mobile -->
<td class="hidden-xs" data-value="{{ row.start_date.format('Y-m-d')|default('0000-00-00') }}">
{% if null != row.limit_id %}
<a href="{{ route('budgets.show.limit', [budget.budget_id, row.limit_id]) }}">
{{ row.start_date.formatLocalized(monthAndDayFormat) }}
&mdash;
{{ row.end_date.formatLocalized(monthAndDayFormat) }}
</a>
{% endif %}
</td>
<!-- budgeted, hidden on mobile -->
<td data-value="{{ row.budgeted|default(0) }}" style="text-align: right;" class="hidden-xs">
{% if null != row.budgeted %}
{{ formatAmountBySymbol(row.budgeted, row.currency_symbol, row.currency_decimal_places) }}
{% endif %}
</td>
<!-- spent, visible on mobile -->
<td data-value="{{ row.spent|default(0) }}" style="text-align: right;">
{{ formatAmountBySymbol(row.spent, row.currency_symbol, row.currency_decimal_places) }}
</td>
<!-- info button, not visible on mobile -->
<td class="hidden-xs">
{% if row.spent != 0 %}
<i class="fa fa-fw text-muted fa-info-circle firefly-info-button"
data-location="budget-spent-amount" data-currency-id="{{ row.currency_id }}" data-budget-id="{{ budget.budget_id }}"></i>
{% endif %}
</td>
<!-- left, hidden on mobile -->
<td data-value="{{ row.left|default(0) }}" style="text-align: right;" class="hidden-xs">
{% if null != row.left %}
{{ formatAmountBySymbol(row.left, row.currency_symbol, row.currency_decimal_places) }}
{% endif %}
</td>
<!-- overspent, visible. -->
<td data-value="{{ row.overspent|default(0) }}" style="text-align: right;">
{% if null != row.overspent %}
{{ formatAmountBySymbol(row.overspent, row.currency_symbol, row.currency_decimal_places) }}
{% endif %}
</td>
</tr>
{% endfor %}
{#
{% set sum_budgeted = sum_budgeted + line.budgeted %}
{% set sum_spent = sum_spent + line.spent %}
{% set sum_left = sum_left + line.left %}
{% set sum_overspent = sum_overspent + line.overspent %}
<tr>
{# Budget name, always visible #}
<!-- Budget name, always visible -->
{% if line.type == 'no-budget' %}
<td data-value="zzzzzzz">
<em>{{ 'no_budget'|_ }}</em>
@ -33,7 +88,7 @@
<a href="{{ route('budgets.show',line.id) }}">{{ line.name }}</a>
</td>
{% endif %}
{# date, hidden on mobile #}
<!-- date, hidden on mobile -->
{% if line.type == 'budget-line' %}
<td class="hidden-xs" data-value="{{ line.start.format('Y-m-d') }}">
<a href="{{ route('budgets.show.limit', [line.id, line.limit]) }}">
@ -48,17 +103,17 @@
</td>
{% endif %}
{# budgeted, hidden on mobile #}
<!-- budgeted, hidden on mobile -->
<td data-value="{{ line.budgeted }}" style="text-align: right;" class="hidden-xs">
{{ line.budgeted|formatAmount }}
</td>
{# spent, visible on mobile #}
<!-- spent, visible on mobile -->
<td data-value="{{ line.spent }}" style="text-align: right;">
{{ line.spent|formatAmount }}
</td>
{# info button, not visible on mobile #}
<!-- info button, not visible on mobile -->
<td class="hidden-xs">
{% if line.spent != 0 %}
<i class="fa fa-fw text-muted fa-info-circle firefly-info-button"
@ -66,37 +121,51 @@
{% endif %}
</td>
{# left, hidden on mobile #}
<!-- left, hidden on mobile -->
<td data-value="{{ line.left }}" style="text-align: right;" class="hidden-xs">
{{ line.left|formatAmount }}
</td>
{# overspent, visible. #}
<!-- overspent, visible. -->
<td data-value="{{ line.overspent }}" style="text-align: right;">
{{ line.overspent|formatAmount }}
</td>
#}
</tr>
{% endfor %}
</tbody>
<tfoot>
{% for sum in budgets.sums %}
<tr>
<td colspan="2"><em>{{ 'sum'|_ }} ({{ sum.currency_name }})</em></td>
<td style="text-align: right;">{{ formatAmountBySymbol(sum.budgeted, sum.currency_symbol, sum.decimal_places) }}</td>
<td style="text-align: right;">{{ formatAmountBySymbol(sum.spent, sum.currency_symbol, sum.decimal_places) }}</td>
<td>&nbsp;</td>
<td style="text-align: right;">{{ formatAmountBySymbol(sum.left, sum.currency_symbol, sum.decimal_places) }}</td>
<td style="text-align: right;">{{ formatAmountBySymbol(sum.overspent, sum.currency_symbol, sum.decimal_places) }}</td>
</tr>
{% endfor %}
{#
<tr>
{# title, visible #}
<!-- title, visible -->
<td><em>{{ 'sum'|_ }}</em></td>
{# date, hidden #}
<!-- date, hidden -->
<td class="hidden-xs">&nbsp;</td>
{# sum of budgeted, hidden #}
<!-- sum of budgeted, hidden -->
<td style="text-align: right;" class="hidden-xs">{{ sum_budgeted|formatAmount }}</td>
{# spent, visible #}
<!-- spent, visible -->
<td style="text-align: right;">{{ sum_spent|formatAmount }}</td>
{# info button, hidden #}
<!-- info button, hidden -->
<td class="hidden-xs">&nbsp;</td>
{# left, hidden #}
<!-- left, hidden -->
<td style="text-align: right;" class="hidden-xs">{{ sum_left|formatAmount }}</td>
<td style="text-align: right;">{{ sum_overspent|formatAmount }}</td>
</tr>
#}
</tfoot>
</table>