mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-02-25 18:45:27 -06:00
Budget box is multi-currency.
This commit is contained in:
parent
b4a732bf77
commit
070f46c755
@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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) }}
|
||||
—
|
||||
{{ 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> </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"> </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"> </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>
|
||||
|
Loading…
Reference in New Issue
Block a user