0 UNION SELECT $next_year"); $view = in_array($_GET['view'] ?? '', ['month', 'edit'], true) ? $_GET['view'] : 'summary'; $can_edit = lookupUserPermission('module_financial') >= 2; if ($view === 'edit' && !$can_edit) { $view = 'summary'; } $current_year = intval(date('Y')); $months_elapsed = reportMonthsElapsed($year); // How far through the year we are, to the day - what the budget should have covered by now $year_elapsed = $year < $current_year ? 1.0 : ($year > $current_year ? 0.0 : (intval(date('z')) + 1) / (intval(date('L')) ? 366 : 365)); // The plan: [category => [1..12 => amount]] $budget = []; $sql = mysqli_query($mysqli, "SELECT budget_category_id, budget_month, SUM(budget_amount) AS amount FROM budget WHERE budget_year = $year GROUP BY budget_category_id, budget_month"); while ($row = mysqli_fetch_assoc($sql)) { $cat = intval($row['budget_category_id']); $budget[$cat] = $budget[$cat] ?? array_fill(1, 12, 0.0); $month = intval($row['budget_month']); if ($month >= 1 && $month <= 12) { $budget[$cat][$month] += floatval($row['amount']); } } $has_prev_budget = intval(dashboardCount("SELECT COUNT(*) FROM budget WHERE budget_year = " . ($year - 1))); // What was spent $actual = reportExpensesByCategory("$year-01-01", "$year-12-31"); $categories = reportCategoryRows('Expense', $actual); foreach ($budget as $cat => $months) { if (!isset($categories[$cat])) { // Budget against a category that is archived or gone - still show it so it can be cleared $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT category_id, category_name, category_color, category_archived_at FROM categories WHERE category_id = $cat")); $categories[$cat] = $row ?: ['category_id' => $cat, 'category_name' => 'Deleted category', 'category_color' => '', 'category_archived_at' => date('Y-m-d')]; } } uasort($categories, function ($a, $b) { return strcasecmp($a['category_name'], $b['category_name']); }); unset($categories[0]); // One line per category $lines = []; foreach ($categories as $cat => $category) { $plan = $budget[$cat] ?? array_fill(1, 12, 0.0); $spent = reportGridRow($actual, $cat); $plan_total = array_sum($plan); $plan_to_date = 0.0; for ($m = 1; $m <= $months_elapsed; $m++) { $plan_to_date += $plan[$m]; } // The month we are in only counts as far as we are through it if ($year === $current_year && $months_elapsed) { $plan_to_date -= $plan[$months_elapsed] * (1 - intval(date('j')) / intval(date('t'))); } $lines[$cat] = [ 'name' => $category['category_name'], 'archived' => !empty($category['category_archived_at']), 'plan' => $plan, 'plan_total' => $plan_total, 'plan_to_date' => $plan_to_date, 'spent' => $spent['months'], 'spent_total' => $spent['total'], ]; } $shown = array_filter($lines, function ($line) { return $line['plan_total'] > 0 || $line['spent_total'] > 0; }); $plan_total = array_sum(array_column($lines, 'plan_total')); $plan_to_date = array_sum(array_column($lines, 'plan_to_date')); $spent_total = array_sum(array_column($lines, 'spent_total')); $over = array_filter($shown, function ($line) { return $line['plan_total'] > 0 && $line['spent_total'] > $line['plan_to_date'] + 0.005; }); $unbudgeted = array_filter($shown, function ($line) { return $line['plan_total'] <= 0 && $line['spent_total'] > 0; }); if (reportExporting()) { $columns = ['name' => ['label' => 'Category']]; for ($m = 1; $m <= 12; $m++) { $columns["b$m"] = ['label' => date('M', mktime(0, 0, 0, $m, 1)) . ' budget', 'type' => 'money']; $columns["a$m"] = ['label' => date('M', mktime(0, 0, 0, $m, 1)) . ' actual', 'type' => 'money']; } $columns['plan_total'] = ['label' => "$year budget", 'type' => 'money']; $columns['spent_total'] = ['label' => "$year actual", 'type' => 'money']; $columns['left'] = ['label' => 'Remaining', 'type' => 'money']; $rows = []; foreach ($shown as $line) { $row = ['name' => $line['name'], 'plan_total' => $line['plan_total'], 'spent_total' => $line['spent_total'], 'left' => $line['plan_total'] - $line['spent_total']]; for ($m = 1; $m <= 12; $m++) { $row["b$m"] = $line['plan'][$m]; $row["a$m"] = $line['spent'][$m]; } $rows[] = $row; } reportCsv("Budget vs Actual $year", $columns, $rows, ['name' => 'Total', 'plan_total' => $plan_total, 'spent_total' => $spent_total, 'left' => $plan_total - $spent_total]); } // Month by month, all categories together $plan_months = array_fill(1, 12, 0.0); $spent_months = array_fill(1, 12, 0.0); foreach ($lines as $line) { for ($m = 1; $m <= 12; $m++) { $plan_months[$m] += $line['plan'][$m]; $spent_months[$m] += $line['spent'][$m]; } } require_once "includes/inc_report_layout.php"; $money_input = function ($value) { return $value > 0 ? number_format($value, 2, '.', '') : ''; }; $used_meter = function ($spent, $plan) { if ($plan <= 0) { return $spent > 0 ? 'No budget' : ''; } $share = $spent / $plan * 100; $tone = $share > 100 ? 'bg-danger' : ($share > 90 ? 'bg-warning' : ''); return '
' . '' . reportPercent(round($share)) . '
'; }; ?>
[$year, $years], 'controls' => reportSegmentControl('view', $view, array_merge(['summary' => 'Summary', 'month' => 'By month'], $can_edit ? ['edit' => 'Edit budget'] : [])), 'export' => $view !== 'edit', ]); ?>

Plan for

$line) { if ($line['archived'] && $line['plan_total'] <= 0) { continue; } ?>
Category Every month
Archived
Total
$year === $current_year ? 'Budget this year' : "Budget $year", 'value' => reportMoney($plan_total), 'sub' => $plan_total > 0 ? escapeHtml(reportMoney($plan_total / 12)) . ' a month on average' : 'No budget set yet'], ['label' => 'Spent', 'value' => reportMoney($spent_total), 'url' => "/agent/expenses.php?canned_date=custom&dtf=$year-01-01&dtt=$year-12-31", 'sub' => $used !== null ? escapeHtml(reportPercent($used)) . ' of the budget' . ($year === $current_year ? ', ' . round($year_elapsed * 100) . '% of the year gone' : '') . '' : ''], ['label' => $year === $current_year ? 'Against budget to date' : 'Against budget', 'value' => reportMoney(abs($plan_to_date - $spent_total)), 'tone' => $plan_total > 0 ? ($spent_total > $plan_to_date ? 'danger' : 'success') : '', 'sub' => $plan_total > 0 ? ($spent_total > $plan_to_date ? 'Over what the budget allows by now' : 'Under what the budget allows by now') : ''], ['label' => 'Remaining this year', 'value' => reportMoney($plan_total - $spent_total), 'tone' => $plan_total - $spent_total < 0 ? 'danger' : '', 'sub' => count($over) ? '' . count($over) . ' categor' . (count($over) === 1 ? 'y' : 'ies') . ' over budget' : 'No category over budget'], ]); ?>
No budget for yet
Set what you plan to spend each month by category, and this report compares it with what you actually spend.
Set the budget
$current_year ? 0 : 12); echo reportChart([ 'type' => 'bar', 'labels' => array_map(function ($m) { return date('M', mktime(0, 0, 0, $m, 1)); }, range(1, 12)), 'series' => [ ['label' => 'Spent', 'data' => array_map(function ($m) use ($spent_months, $months_shown) { return $m <= $months_shown ? round($spent_months[$m], 2) : null; }, range(1, 12)), 'tone' => 'accent'], ['label' => 'Budget', 'data' => array_map(function ($m) use ($plan_months) { return round($plan_months[$m], 2); }, range(1, 12)), 'tone' => 'muted', 'type' => 'line', 'dashed' => true], ], 'format' => 'money', ], '', 'Spending per month against the budget'); ?>
$line) { ?>
Category
0 ? reportMoneyWhole($plan) : ' ' ?>
0 ? reportMoneyWhole($line['plan_total']) : ' ' ?>
No budget and no spending in .
Total
0 ? reportMoneyWhole($plan_months[$m]) : ' ' ?>
['label' => 'Category', 'html' => function ($row) { return escapeHtml($row['name']) . ($row['archived'] ? ' Archived' : ''); }], 'plan_total' => ['label' => "Budget $year", 'type' => 'money', 'html' => function ($row) { return $row['plan_total'] > 0 ? reportMoney($row['plan_total']) : '–'; }], 'plan_to_date' => ['label' => $year === $current_year ? 'Budget to date' : 'Budget', 'type' => 'money', 'title' => 'What the budget allows up to today', 'html' => function ($row) { return $row['plan_total'] > 0 ? reportMoney($row['plan_to_date']) : '–'; }], 'spent_total' => ['label' => 'Spent', 'type' => 'money', 'html' => function ($row) use ($year) { if (abs($row['spent_total']) < 0.005) { return '–'; } return '" . reportMoney($row['spent_total']) . ''; }], 'difference' => ['label' => 'Under / over', 'type' => 'money', 'title' => 'Budget to date less what was spent - red is over', 'html' => function ($row) { if ($row['plan_total'] <= 0) { return '–'; } return '' . reportMoney($row['difference']) . ''; }], 'remaining' => ['label' => 'Left this year', 'type' => 'money', 'html' => function ($row) { if ($row['plan_total'] <= 0) { return '–'; } return '' . reportMoney($row['remaining']) . ''; }], 'used' => ['label' => 'Used', 'type' => 'percent', 'class' => 'itflow-report-used', 'html' => function ($row) use ($used_meter) { return $used_meter($row['spent_total'], $row['plan_total']); }], ]; if ($year !== $current_year) { // A past year is all "to date", a future one has none yet unset($columns['plan_to_date']); $columns['difference']['title'] = 'The budget less what was spent - red is over'; } $rows = []; foreach ($shown as $cat => $line) { $rows[] = [ 'cat' => $cat, 'name' => $line['name'], 'archived' => $line['archived'], 'plan_total' => $line['plan_total'], 'plan_to_date' => $line['plan_to_date'], 'spent_total' => $line['spent_total'], 'difference' => $line['plan_to_date'] - $line['spent_total'], 'remaining' => $line['plan_total'] - $line['spent_total'], 'used' => reportShare($line['spent_total'], $line['plan_total']), ]; } ?> ' . count($unbudgeted) . ' categor' . (count($unbudgeted) === 1 ? 'y has' : 'ies have') . ' spending but no budget' : '') ?> "No budget and no spending in $year.", 'totals' => ['name' => 'Total', 'plan_total' => $plan_total, 'plan_to_date' => $plan_to_date, 'spent_total' => $spent_total, 'difference' => $plan_to_date - $spent_total, 'remaining' => $plan_total - $spent_total], 'totals_html' => ['used' => $used_meter($spent_total, $plan_total), 'difference' => '' . reportMoney($plan_to_date - $spent_total) . ''], ]) ?>