diff --git a/app/Generator/Report/Audit/MonthReportGenerator.php b/app/Generator/Report/Audit/MonthReportGenerator.php
index f5ab29a5bb..cc31219d9e 100644
--- a/app/Generator/Report/Audit/MonthReportGenerator.php
+++ b/app/Generator/Report/Audit/MonthReportGenerator.php
@@ -25,7 +25,7 @@ use Steam;
/**
* Class MonthReportGenerator
*
- * @package FireflyIII\Generator\Report\Standard
+ * @package FireflyIII\Generator\Report\Audit
*/
class MonthReportGenerator implements ReportGeneratorInterface
{
diff --git a/app/Generator/Report/Budget/MonthReportGenerator.php b/app/Generator/Report/Budget/MonthReportGenerator.php
new file mode 100644
index 0000000000..10b5c39afe
--- /dev/null
+++ b/app/Generator/Report/Budget/MonthReportGenerator.php
@@ -0,0 +1,329 @@
+income = new Collection;
+ $this->expenses = new Collection;
+ }
+
+ /**
+ * @return string
+ */
+ public function generate(): string
+ {
+ $accountIds = join(',', $this->accounts->pluck('id')->toArray());
+ $categoryIds = join(',', $this->budgets->pluck('id')->toArray());
+ $reportType = 'budget';
+ // $expenses = $this->getExpenses();
+ // $income = $this->getIncome();
+ // $accountSummary = $this->getObjectSummary($this->summarizeByAccount($expenses), $this->summarizeByAccount($income));
+ // $categorySummary = $this->getObjectSummary($this->summarizeByCategory($expenses), $this->summarizeByCategory($income));
+ // $averageExpenses = $this->getAverages($expenses, SORT_ASC);
+ // $averageIncome = $this->getAverages($income, SORT_DESC);
+ // $topExpenses = $this->getTopExpenses();
+ // $topIncome = $this->getTopIncome();
+
+
+ // render!
+ return view(
+ 'reports.budget.month',
+ compact(
+ 'accountIds', 'categoryIds', 'topIncome', 'reportType', 'accountSummary', 'categorySummary', 'averageExpenses', 'averageIncome', 'topExpenses'
+ )
+ )
+ ->with('start', $this->start)->with('end', $this->end)
+ ->with('budgets', $this->budgets)
+ ->with('accounts', $this->accounts)
+ ->render();
+ }
+
+ /**
+ * @param Collection $accounts
+ *
+ * @return ReportGeneratorInterface
+ */
+ public function setAccounts(Collection $accounts): ReportGeneratorInterface
+ {
+ $this->accounts = $accounts;
+
+ return $this;
+ }
+
+ /**
+ * @param Collection $budgets
+ *
+ * @return ReportGeneratorInterface
+ */
+ public function setBudgets(Collection $budgets): ReportGeneratorInterface
+ {
+ $this->budgets = $budgets;
+
+ return $this;
+ }
+
+ /**
+ * @param Collection $categories
+ *
+ * @return ReportGeneratorInterface
+ */
+ public function setCategories(Collection $categories): ReportGeneratorInterface
+ {
+ $this->categories = $categories;
+
+ return $this;
+ }
+
+ /**
+ * @param Carbon $date
+ *
+ * @return ReportGeneratorInterface
+ */
+ public function setEndDate(Carbon $date): ReportGeneratorInterface
+ {
+ $this->end = $date;
+
+ return $this;
+ }
+
+ /**
+ * @param Carbon $date
+ *
+ * @return ReportGeneratorInterface
+ */
+ public function setStartDate(Carbon $date): ReportGeneratorInterface
+ {
+ $this->start = $date;
+
+ return $this;
+ }
+
+ /**
+ * @param Collection $collection
+ * @param int $sortFlag
+ *
+ * @return array
+ */
+ private function getAverages(Collection $collection, int $sortFlag): array
+ {
+ $result = [];
+ /** @var Transaction $transaction */
+ foreach ($collection as $transaction) {
+ // opposing name and ID:
+ $opposingId = $transaction->opposing_account_id;
+
+ // is not set?
+ if (!isset($result[$opposingId])) {
+ $name = $transaction->opposing_account_name;
+ $result[$opposingId] = [
+ 'name' => $name,
+ 'count' => 1,
+ 'id' => $opposingId,
+ 'average' => $transaction->transaction_amount,
+ 'sum' => $transaction->transaction_amount,
+ ];
+ continue;
+ }
+ $result[$opposingId]['count']++;
+ $result[$opposingId]['sum'] = bcadd($result[$opposingId]['sum'], $transaction->transaction_amount);
+ $result[$opposingId]['average'] = bcdiv($result[$opposingId]['sum'], strval($result[$opposingId]['count']));
+ }
+
+ // sort result by average:
+ $average = [];
+ foreach ($result as $key => $row) {
+ $average[$key] = floatval($row['average']);
+ }
+
+ array_multisort($average, $sortFlag, $result);
+
+ return $result;
+ }
+
+ /**
+ * @return Collection
+ */
+ private function getExpenses(): Collection
+ {
+ if ($this->expenses->count() > 0) {
+ Log::debug('Return previous set of expenses.');
+
+ return $this->expenses;
+ }
+
+ $collector = new JournalCollector(auth()->user());
+ $collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
+ ->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
+ ->setCategories($this->categories)->withOpposingAccount()->disableFilter();
+
+ $accountIds = $this->accounts->pluck('id')->toArray();
+ $transactions = $collector->getJournals();
+ $transactions = self::filterExpenses($transactions, $accountIds);
+ $this->expenses = $transactions;
+
+ return $transactions;
+ }
+
+ /**
+ * @return Collection
+ */
+ private function getIncome(): Collection
+ {
+ if ($this->income->count() > 0) {
+ return $this->income;
+ }
+
+ $collector = new JournalCollector(auth()->user());
+ $collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
+ ->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
+ ->setCategories($this->categories)->withOpposingAccount();
+ $accountIds = $this->accounts->pluck('id')->toArray();
+ $transactions = $collector->getJournals();
+ $transactions = self::filterIncome($transactions, $accountIds);
+ $this->income = $transactions;
+
+ return $transactions;
+ }
+
+ /**
+ * @param array $spent
+ * @param array $earned
+ *
+ * @return array
+ */
+ private function getObjectSummary(array $spent, array $earned): array
+ {
+ $return = [];
+
+ /**
+ * @var int $accountId
+ * @var string $entry
+ */
+ foreach ($spent as $objectId => $entry) {
+ if (!isset($return[$objectId])) {
+ $return[$objectId] = ['spent' => 0, 'earned' => 0];
+ }
+
+ $return[$objectId]['spent'] = $entry;
+ }
+ unset($entry);
+
+ /**
+ * @var int $accountId
+ * @var string $entry
+ */
+ foreach ($earned as $objectId => $entry) {
+ if (!isset($return[$objectId])) {
+ $return[$objectId] = ['spent' => 0, 'earned' => 0];
+ }
+
+ $return[$objectId]['earned'] = $entry;
+ }
+
+
+ return $return;
+ }
+
+
+ /**
+ * @return Collection
+ */
+ private function getTopExpenses(): Collection
+ {
+ $transactions = $this->getExpenses()->sortBy('transaction_amount');
+
+ return $transactions;
+ }
+
+ /**
+ * @return Collection
+ */
+ private function getTopIncome(): Collection
+ {
+ $transactions = $this->getIncome()->sortByDesc('transaction_amount');
+
+ return $transactions;
+ }
+
+ /**
+ * @param Collection $collection
+ *
+ * @return array
+ */
+ private function summarizeByAccount(Collection $collection): array
+ {
+ $result = [];
+ /** @var Transaction $transaction */
+ foreach ($collection as $transaction) {
+ $accountId = $transaction->account_id;
+ $result[$accountId] = $result[$accountId] ?? '0';
+ $result[$accountId] = bcadd($transaction->transaction_amount, $result[$accountId]);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param Collection $collection
+ *
+ * @return array
+ */
+ private function summarizeByCategory(Collection $collection): array
+ {
+ $result = [];
+ /** @var Transaction $transaction */
+ foreach ($collection as $transaction) {
+ $jrnlCatId = intval($transaction->transaction_journal_category_id);
+ $transCatId = intval($transaction->transaction_category_id);
+ $categoryId = max($jrnlCatId, $transCatId);
+ $result[$categoryId] = $result[$categoryId] ?? '0';
+ $result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]);
+ }
+
+ return $result;
+ }
+}
\ No newline at end of file
diff --git a/app/Generator/Report/Budget/MultiYearReportGenerator.php b/app/Generator/Report/Budget/MultiYearReportGenerator.php
new file mode 100644
index 0000000000..3d92def9f3
--- /dev/null
+++ b/app/Generator/Report/Budget/MultiYearReportGenerator.php
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+ {{ 'name'|_ }} |
+ {{ 'earned'|_ }} |
+ {{ 'spent'|_ }} |
+
+
+
+ {% for account in accounts %}
+
+
+ {{ account.name }}
+ |
+ {% if accountSummary[account.id] %}
+ {{ accountSummary[account.id].earned|formatAmount }} |
+ {% else %}
+ {{ 0|formatAmount }} |
+ {% endif %}
+ {% if accountSummary[account.id] %}
+ {{ accountSummary[account.id].spent|formatAmount }} |
+ {% else %}
+ {{ 0|formatAmount }} |
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'name'|_ }} |
+ {{ 'spent'|_ }} |
+
+
+
+ {% for budget in budgets %}
+
+
+ {{ budget.name }}
+ |
+ {% if budgetSummary[budget.id] %}
+ {{ budgetSummary[budget.id].earned|formatAmount }} |
+ {% else %}
+ {{ 0|formatAmount }} |
+ {% endif %}
+ {% if budgetSummary[budget.id] %}
+ {{ budgetSummary[budget.id].spent|formatAmount }} |
+ {% else %}
+ {{ 0|formatAmount }} |
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+ {% if budgets.count > 1 %}
+
+
+
+
+
+
+
+
+
+ {% endif %}
+ {% if accounts.count > 1 %}
+
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+ {% if averageExpenses|length > 0 %}
+
+
+
+
+
+
+
+ {{ 'account'|_ }} |
+ {{ 'spent_average'|_ }} |
+ {{ 'total'|_ }} |
+ {{ 'transaction_count'|_ }} |
+
+
+
+ {% for row in averageExpenses %}
+
+
+ {{ row.name }}
+ |
+
+ {{ row.average|formatAmount }}
+ |
+
+ {{ row.sum|formatAmount }}
+ |
+
+ {{ row.count }}
+ |
+
+ {% endfor %}
+
+
+
+
+
+ {% endif %}
+ {% if topExpenses.count > 0 %}
+
+ {% endif %}
+
+
+{% endblock %}
+
+{% block scripts %}
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block styles %}
+
+{% endblock %}
\ No newline at end of file