Skip to content

Commit 48c6ba0

Browse files
author
Adam
committed
feat: Add "Exclude from reports" flag for categories
Categories can now be marked as excluded from reports. Excluded categories are hidden from budget analysis, spending reports, dashboard totals, and budget alerts. Useful for investment adjustments, internal bookkeeping, reimbursements, or any category where transactions shouldn't count as real income or expenses. - New excluded_from_reports column on budget_categories (migration 059) - Checkbox in category create/edit form - Filtering in CategoryService, BudgetAlertService, ReportAggregator
1 parent 25bdfd3 commit 48c6ba0

8 files changed

Lines changed: 102 additions & 9 deletions

File tree

budget/lib/Controller/CategoryController.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ public function create(
149149
?string $icon = null,
150150
?string $color = null,
151151
?float $budgetAmount = null,
152-
int $sortOrder = 0
152+
int $sortOrder = 0,
153+
bool $excludedFromReports = false
153154
): DataResponse {
154155
try {
155156
// Validate name (required)
@@ -190,7 +191,8 @@ public function create(
190191
$icon,
191192
$color,
192193
$budgetAmount,
193-
$sortOrder
194+
$sortOrder,
195+
$excludedFromReports
194196
);
195197
return new DataResponse($category, Http::STATUS_CREATED);
196198
} catch (\Exception $e) {
@@ -274,6 +276,9 @@ public function update(
274276
if ($sortOrder !== null) {
275277
$updates['sortOrder'] = $sortOrder;
276278
}
279+
if (array_key_exists('excludedFromReports', $params)) {
280+
$updates['excludedFromReports'] = (bool) $params['excludedFromReports'];
281+
}
277282

278283
if (empty($updates)) {
279284
return new DataResponse(['error' => $this->l->t('No valid fields to update')], Http::STATUS_BAD_REQUEST);

budget/lib/Db/Category.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* @method void setCreatedAt(string $createdAt)
3333
* @method string|null getUpdatedAt()
3434
* @method void setUpdatedAt(string $updatedAt)
35+
* @method bool|null getExcludedFromReports()
36+
* @method void setExcludedFromReports(?bool $excludedFromReports)
3537
*/
3638
class Category extends Entity implements JsonSerializable {
3739
protected $userId;
@@ -43,6 +45,7 @@ class Category extends Entity implements JsonSerializable {
4345
protected $budgetAmount;
4446
protected $budgetPeriod; // monthly, weekly, yearly, quarterly
4547
protected $sortOrder;
48+
protected $excludedFromReports;
4649
protected $createdAt;
4750
protected $updatedAt;
4851

@@ -51,6 +54,7 @@ public function __construct() {
5154
$this->addType('parentId', 'integer');
5255
$this->addType('budgetAmount', 'float');
5356
$this->addType('sortOrder', 'integer');
57+
$this->addType('excludedFromReports', 'boolean');
5458
}
5559

5660
/**
@@ -69,6 +73,7 @@ public function jsonSerialize(): array {
6973
'budgetAmount' => $this->getBudgetAmount(),
7074
'budgetPeriod' => $this->getBudgetPeriod() ?? 'monthly',
7175
'sortOrder' => $this->getSortOrder(),
76+
'excludedFromReports' => $this->getExcludedFromReports() ?? false,
7277
'createdAt' => $this->getCreatedAt(),
7378
'updatedAt' => $this->getUpdatedAt(),
7479
];
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OCA\Budget\Migration;
6+
7+
use Closure;
8+
use OCP\DB\ISchemaWrapper;
9+
use OCP\DB\Types;
10+
use OCP\Migration\IOutput;
11+
use OCP\Migration\SimpleMigrationStep;
12+
13+
/**
14+
* Add excluded_from_reports flag to budget_categories.
15+
* Categories with this flag are excluded from budget calculations,
16+
* spending reports, and dashboard totals.
17+
*/
18+
class Version001000059Date20260502 extends SimpleMigrationStep {
19+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
20+
/** @var ISchemaWrapper $schema */
21+
$schema = $schemaClosure();
22+
23+
$table = $schema->getTable('budget_categories');
24+
if (!$table->hasColumn('excluded_from_reports')) {
25+
$table->addColumn('excluded_from_reports', Types::BOOLEAN, [
26+
'notnull' => false,
27+
'default' => false,
28+
]);
29+
}
30+
31+
return $schema;
32+
}
33+
}

budget/lib/Service/BudgetAlertService.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,12 @@ public function getAlerts(string $userId): array {
4747
$currentMonth = date('Y-m');
4848
$snapshotOverrides = $this->budgetSnapshotMapper->findEffectiveBatch($userId, $currentMonth);
4949

50-
// Filter to categories with effective budgets > 0
50+
// Filter to categories with effective budgets > 0 (excluding excluded categories)
5151
$categoriesWithBudgets = [];
5252
foreach ($categories as $category) {
53+
if ($category->getExcludedFromReports()) {
54+
continue;
55+
}
5356
$catId = $category->getId();
5457
$amount = isset($snapshotOverrides[$catId])
5558
? (float) ($snapshotOverrides[$catId]['amount'] ?? 0)
@@ -137,9 +140,12 @@ public function getBudgetStatus(string $userId): array {
137140
$currentMonth = date('Y-m');
138141
$snapshotOverrides = $this->budgetSnapshotMapper->findEffectiveBatch($userId, $currentMonth);
139142

140-
// Filter to categories with effective budgets > 0
143+
// Filter to categories with effective budgets > 0 (excluding excluded categories)
141144
$categoriesWithBudgets = [];
142145
foreach ($categories as $category) {
146+
if ($category->getExcludedFromReports()) {
147+
continue;
148+
}
143149
$catId = $category->getId();
144150
$amount = isset($snapshotOverrides[$catId])
145151
? (float) ($snapshotOverrides[$catId]['amount'] ?? 0)

budget/lib/Service/CategoryService.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ public function create(
6464
?string $icon = null,
6565
?string $color = null,
6666
?float $budgetAmount = null,
67-
int $sortOrder = 0
67+
int $sortOrder = 0,
68+
bool $excludedFromReports = false
6869
): Category {
6970
// Validate parent if provided
7071
if ($parentId !== null) {
@@ -85,6 +86,7 @@ public function create(
8586
$category->setColor($color ?: $this->generateRandomColor());
8687
$category->setBudgetAmount($budgetAmount);
8788
$category->setSortOrder($sortOrder);
89+
$category->setExcludedFromReports($excludedFromReports);
8890
$this->setTimestamps($category, true);
8991

9092
return $this->mapper->insert($category);
@@ -426,6 +428,9 @@ public function getBudgetAnalysis(string $userId, ?string $month = null): array
426428
$expenseCategoryIds = [];
427429
$incomeCategoryIds = [];
428430
foreach ($categories as $category) {
431+
if ($category->getExcludedFromReports()) {
432+
continue;
433+
}
429434
$catId = $category->getId();
430435
$budget = $effectiveBudgets[$catId]['amount'] ?? 0;
431436
if ($budget > 0) {
@@ -448,6 +453,9 @@ public function getBudgetAnalysis(string $userId, ?string $month = null): array
448453

449454
$analysis = [];
450455
foreach ($categories as $category) {
456+
if ($category->getExcludedFromReports()) {
457+
continue;
458+
}
451459
$catId = $category->getId();
452460
$budget = (float) ($effectiveBudgets[$catId]['amount'] ?? 0);
453461
if ($budget > 0) {

budget/lib/Service/Report/ReportAggregator.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ public function generateSummary(
207207

208208
$excludeTransfers = $accountId === null;
209209

210-
// Get spending breakdown
211-
$summary['spending'] = $this->transactionMapper->getSpendingSummary(
210+
// Get spending breakdown (filter out excluded categories)
211+
$spending = $this->transactionMapper->getSpendingSummary(
212212
$userId,
213213
$startDate,
214214
$endDate,
@@ -218,6 +218,20 @@ public function generateSummary(
218218
$excludeTransfers
219219
);
220220

221+
$allCategories = $this->categoryMapper->findAll($userId);
222+
$excludedCategoryIds = [];
223+
foreach ($allCategories as $cat) {
224+
if ($cat->getExcludedFromReports()) {
225+
$excludedCategoryIds[$cat->getId()] = true;
226+
}
227+
}
228+
if (!empty($excludedCategoryIds)) {
229+
$spending = array_values(array_filter($spending, function ($item) use ($excludedCategoryIds) {
230+
return !isset($excludedCategoryIds[$item['categoryId'] ?? 0]);
231+
}));
232+
}
233+
$summary['spending'] = $spending;
234+
221235
// Generate trend data (with currency conversion for multi-account view)
222236
$summary['trends'] = $this->generateTrendData($userId, $accountId, $startDate, $endDate, $tagIds, $includeUntagged);
223237

@@ -302,10 +316,13 @@ public function getBudgetReport(string $userId, string $startDate, string $endDa
302316
$reportMonth = substr($startDate, 0, 7); // YYYY-MM from startDate
303317
$snapshotOverrides = $this->budgetSnapshotMapper->findEffectiveBatch($userId, $reportMonth);
304318

305-
// Collect category IDs that have budgets (considering snapshots)
319+
// Collect category IDs that have budgets (considering snapshots, excluding excluded)
306320
$categoryIds = [];
307321
$resolvedBudgets = [];
308322
foreach ($categories as $category) {
323+
if ($category->getExcludedFromReports()) {
324+
continue;
325+
}
309326
$catId = $category->getId();
310327
$budgeted = isset($snapshotOverrides[$catId])
311328
? (float) ($snapshotOverrides[$catId]['amount'] ?? 0)

budget/src/modules/categories/CategoriesModule.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,9 @@ export default class CategoriesModule {
948948

949949
const colorInput = document.getElementById('category-color');
950950
if (colorInput) colorInput.value = '#3b82f6';
951+
952+
const excludedCheckbox = document.getElementById('category-excluded-from-reports');
953+
if (excludedCheckbox) excludedCheckbox.checked = false;
951954
}
952955

953956
loadCategoryData(category) {
@@ -957,6 +960,11 @@ export default class CategoriesModule {
957960
document.getElementById('category-parent').value = category.parentId || '';
958961
document.getElementById('category-color').value = category.color || '#3b82f6';
959962

963+
const excludedCheckbox = document.getElementById('category-excluded-from-reports');
964+
if (excludedCheckbox) {
965+
excludedCheckbox.checked = category.excludedFromReports || false;
966+
}
967+
960968
// Load tag sets for this category
961969
this.app.renderCategoryTagSetsUI(category.id);
962970
}
@@ -991,11 +999,14 @@ export default class CategoriesModule {
991999
return;
9921000
}
9931001

1002+
const excludedFromReports = document.getElementById('category-excluded-from-reports')?.checked || false;
1003+
9941004
const categoryData = {
9951005
name,
9961006
type,
9971007
parentId: parentId ? parseInt(parentId) : null,
998-
color
1008+
color,
1009+
excludedFromReports
9991010
};
10001011

10011012
try {

budget/templates/index.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5524,6 +5524,14 @@ class="app-navigation-search-clear icon-close"
55245524
<small id="category-color-help" class="form-text"><?php p($l->t('Color for charts and display')); ?></small>
55255525
</div>
55265526

5527+
<div class="form-group">
5528+
<label class="checkbox-label">
5529+
<input type="checkbox" id="category-excluded-from-reports">
5530+
<?php p($l->t('Exclude from reports')); ?>
5531+
</label>
5532+
<small class="form-text"><?php p($l->t('Transactions in this category will not count toward budgets, spending reports, or dashboard totals.')); ?></small>
5533+
</div>
5534+
55275535
<!-- Tag Sets Container -->
55285536
<div id="category-tag-sets-container"></div>
55295537

0 commit comments

Comments
 (0)