Phase G: Add Budgeting and Year-End Close
Budgeting: - Budget + BudgetLine entities with Jan–Dec monthly columns per GL account - BudgetsController: Index, Create, Edit, SetDefault, Copy, Delete - Copy action rolls a budget forward to a new fiscal year - Budget vs. Actual report (BudgetVsActual): compares monthly budget amounts to real P&L by calling GetProfitAndLossAsync once per month; variance shown as favorable/unfavorable; year + budget selectors in header - Views: Budgets/Index, Create, Edit with inline annual totals via budget-edit.js - Nav link + report card on Landing Year-End Close: - YearEndClose entity records each closed year + JE reference for audit trail - AccountsController.YearEndClose GET (history + form) + CloseYear POST - Close zeroes all Revenue and Expense/COGS account balances into Retained Earnings via IAccountBalanceService and posts a supporting JE dated Dec 31 - Idempotency: rejects attempt to close an already-closed year - Pre-close checklist in view to guide the workflow - Nav link under Finance Migration AddBudgetsAndYearEndClose applied Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
@using PowderCoating.Web.Controllers
|
||||
@using PowderCoating.Core.Enums
|
||||
@model List<BudgetVsActualRow>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Budget vs. Actual";
|
||||
ViewData["PageIcon"] = "bi-bar-chart-line";
|
||||
var reportYear = (int)ViewBag.ReportYear;
|
||||
var budget = ViewBag.Budget as PowderCoating.Core.Entities.Budget;
|
||||
var allBudgets = ViewBag.AllBudgets as List<PowderCoating.Core.Entities.Budget> ?? new();
|
||||
var noBudget = ViewBag.NoBudget == true;
|
||||
var availYears = ViewBag.AvailableYears as List<int> ?? new();
|
||||
var months = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<form method="get" asp-action="BudgetVsActual" class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<select name="year" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()">
|
||||
@foreach (var y in availYears)
|
||||
{
|
||||
<option value="@y" selected="@(y == reportYear ? "selected" : null)">@y</option>
|
||||
}
|
||||
</select>
|
||||
@if (allBudgets.Count > 1)
|
||||
{
|
||||
<select name="budgetId" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()">
|
||||
@foreach (var b in allBudgets)
|
||||
{
|
||||
<option value="@b.Id" selected="@(budget?.Id == b.Id ? "selected" : null)">
|
||||
@b.Name@(b.IsDefault ? " (default)" : "")
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
</form>
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-controller="Budgets" asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-pie-chart me-1"></i>Manage Budgets
|
||||
</a>
|
||||
<a asp-action="Landing" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Reports
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (noBudget || budget == null)
|
||||
{
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center text-muted py-5">
|
||||
<i class="bi bi-bar-chart-line display-4 d-block mb-3 opacity-25"></i>
|
||||
<p>No budget found for <strong>@reportYear</strong>.</p>
|
||||
<a asp-controller="Budgets" asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-2"></i>Create Budget for @reportYear
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
var revRows = Model.Where(r => r.AccountType == AccountType.Revenue).ToList();
|
||||
var expRows = Model.Where(r => r.AccountType != AccountType.Revenue).ToList();
|
||||
var totalBudgetRev = revRows.Sum(r => r.BudgetAnnual);
|
||||
var totalActualRev = revRows.Sum(r => r.ActualAnnual);
|
||||
var totalBudgetExp = expRows.Sum(r => r.BudgetAnnual);
|
||||
var totalActualExp = expRows.Sum(r => r.ActualAnnual);
|
||||
var budgetNetIncome = totalBudgetRev - totalBudgetExp;
|
||||
var actualNetIncome = totalActualRev - totalActualExp;
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center">
|
||||
<div class="card-body py-3">
|
||||
<div class="text-muted small">Budget Revenue</div>
|
||||
<div class="fs-4 fw-bold text-success">@totalBudgetRev.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center">
|
||||
<div class="card-body py-3">
|
||||
<div class="text-muted small">Actual Revenue</div>
|
||||
<div class="fs-4 fw-bold @(totalActualRev >= totalBudgetRev ? "text-success" : "text-warning")">@totalActualRev.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center">
|
||||
<div class="card-body py-3">
|
||||
<div class="text-muted small">Budget Net Income</div>
|
||||
<div class="fs-4 fw-bold">@budgetNetIncome.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm text-center">
|
||||
<div class="card-body py-3">
|
||||
<div class="text-muted small">Actual Net Income</div>
|
||||
<div class="fs-4 fw-bold @(actualNetIncome >= budgetNetIncome ? "text-success" : "text-danger")">@actualNetIncome.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-semibold">
|
||||
<i class="bi bi-bar-chart-line me-2 text-primary"></i>@budget.Name — @reportYear
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="min-width:200px">Account</th>
|
||||
<th class="text-end">Budget Annual</th>
|
||||
<th class="text-end">Actual Annual</th>
|
||||
<th class="text-end">Variance</th>
|
||||
@foreach (var m in months)
|
||||
{
|
||||
<th class="text-end small" style="min-width:60px">@m</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (revRows.Any())
|
||||
{
|
||||
<tr class="table-success">
|
||||
<td colspan="@(4 + 12)" class="fw-bold py-1 ps-3 small">REVENUE</td>
|
||||
</tr>
|
||||
@foreach (var row in revRows)
|
||||
{
|
||||
var varA = row.VarianceAnnual;
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-semibold">@row.AccountNumber</span>
|
||||
<span class="text-muted ms-1">@row.AccountName</span>
|
||||
</td>
|
||||
<td class="text-end">@row.BudgetAnnual.ToString("C")</td>
|
||||
<td class="text-end">@row.ActualAnnual.ToString("C")</td>
|
||||
<td class="text-end fw-semibold @(varA >= 0 ? "text-success" : "text-danger")">
|
||||
@(varA >= 0 ? "+" : "")@varA.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
var varM = row.ActualMonths[m] - row.BudgetMonths[m];
|
||||
<td class="text-end small @(varM < 0 ? "text-danger" : "")">
|
||||
@row.ActualMonths[m].ToString("N0")
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
<tr class="fw-bold table-success">
|
||||
<td>Total Revenue</td>
|
||||
<td class="text-end">@totalBudgetRev.ToString("C")</td>
|
||||
<td class="text-end">@totalActualRev.ToString("C")</td>
|
||||
<td class="text-end @((totalActualRev - totalBudgetRev) >= 0 ? "text-success" : "text-danger")">
|
||||
@{var revVar = totalActualRev - totalBudgetRev;}
|
||||
@(revVar >= 0 ? "+" : "")@revVar.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
<td class="text-end small">@revRows.Sum(r => r.ActualMonths[m]).ToString("N0")</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
@if (expRows.Any())
|
||||
{
|
||||
<tr class="table-danger">
|
||||
<td colspan="@(4 + 12)" class="fw-bold py-1 ps-3 small">EXPENSE</td>
|
||||
</tr>
|
||||
@foreach (var row in expRows)
|
||||
{
|
||||
var varA = row.BudgetAnnual - row.ActualAnnual; // favorable if under budget
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-semibold">@row.AccountNumber</span>
|
||||
<span class="text-muted ms-1">@row.AccountName</span>
|
||||
</td>
|
||||
<td class="text-end">@row.BudgetAnnual.ToString("C")</td>
|
||||
<td class="text-end">@row.ActualAnnual.ToString("C")</td>
|
||||
<td class="text-end fw-semibold @(varA >= 0 ? "text-success" : "text-danger")">
|
||||
@(varA >= 0 ? "+" : "")@varA.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
var varM = row.BudgetMonths[m] - row.ActualMonths[m];
|
||||
<td class="text-end small @(varM < 0 ? "text-danger" : "")">
|
||||
@row.ActualMonths[m].ToString("N0")
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
<tr class="fw-bold table-danger">
|
||||
<td>Total Expenses</td>
|
||||
<td class="text-end">@totalBudgetExp.ToString("C")</td>
|
||||
<td class="text-end">@totalActualExp.ToString("C")</td>
|
||||
<td class="text-end @((totalBudgetExp - totalActualExp) >= 0 ? "text-success" : "text-danger")">
|
||||
@{var expVar = totalBudgetExp - totalActualExp;}
|
||||
@(expVar >= 0 ? "+" : "")@expVar.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
<td class="text-end small">@expRows.Sum(r => r.ActualMonths[m]).ToString("N0")</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
<!-- Net Income row -->
|
||||
<tr class="fw-bold border-top" style="border-top-width:2px!important">
|
||||
<td>Net Income</td>
|
||||
<td class="text-end">@budgetNetIncome.ToString("C")</td>
|
||||
<td class="text-end @(actualNetIncome >= 0 ? "text-success" : "text-danger")">@actualNetIncome.ToString("C")</td>
|
||||
<td class="text-end @((actualNetIncome - budgetNetIncome) >= 0 ? "text-success" : "text-danger")">
|
||||
@{var netVar = actualNetIncome - budgetNetIncome;}
|
||||
@(netVar >= 0 ? "+" : "")@netVar.ToString("C")
|
||||
</td>
|
||||
@for (int m = 0; m < 12; m++)
|
||||
{
|
||||
var mActualNet = revRows.Sum(r => r.ActualMonths[m]) - expRows.Sum(r => r.ActualMonths[m]);
|
||||
<td class="text-end small @(mActualNet < 0 ? "text-danger" : "")">@mActualNet.ToString("N0")</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-muted small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Variance is shown as <strong>favorable</strong> (positive) when revenue exceeds budget or expenses are under budget.
|
||||
Actual figures reflect the P&L for each calendar month.
|
||||
</div>
|
||||
}
|
||||
@@ -220,6 +220,14 @@
|
||||
<p>Payments to 1099-eligible vendors by calendar year — flags those exceeding the $600 reporting threshold.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
<a asp-controller="Reports" asp-action="BudgetVsActual" class="report-card">
|
||||
<div class="report-card-icon" style="background:#f0fdf4;color:#15803d;">
|
||||
<i class="bi bi-bar-chart-line"></i>
|
||||
</div>
|
||||
<h5>Budget vs. Actual</h5>
|
||||
<p>Compare monthly budgeted amounts against real P&L activity — revenue, expenses, and net income variance.</p>
|
||||
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user