From 3332f58376639a8c313f8cdc14bb3c6e82d4b4fd Mon Sep 17 00:00:00 2001
From: Neil <55785687+carkom@users.noreply.github.com>
Date: Mon, 24 Jun 2024 20:08:59 +0100
Subject: [PATCH] Custom Reports: Adjust Net values (#2871)

* Add Net value

* notes

* fix

* revert changes

* balanceTypeOpType

* lint fix

* add net numbers

* bar fix

* nit fixes and fix clicks

* remove abs
---
 .../src/components/reports/ReportOptions.ts   |  2 +
 .../src/components/reports/ReportSummary.tsx  | 17 ++++---
 .../src/components/reports/disabledList.ts    |  8 ++--
 .../components/reports/graphs/AreaGraph.tsx   | 25 +++++++++--
 .../components/reports/graphs/BarGraph.tsx    | 35 ++++++++++++---
 .../components/reports/graphs/DonutGraph.tsx  |  9 ++--
 .../components/reports/graphs/LineGraph.tsx   |  7 ++-
 .../reports/graphs/StackedBarGraph.tsx        |  8 +++-
 .../components/reports/graphs/showActivity.ts | 12 ++---
 .../reports/graphs/tableGraph/ReportTable.tsx |  3 +-
 .../graphs/tableGraph/ReportTableHeader.tsx   |  7 ++-
 .../graphs/tableGraph/ReportTableList.tsx     |  2 +
 .../graphs/tableGraph/ReportTableRow.tsx      |  7 ++-
 .../graphs/tableGraph/ReportTableTotals.tsx   |  2 +
 .../reports/reports/CustomReport.tsx          |  5 ++-
 .../reports/spreadsheets/calculateLegend.ts   |  3 +-
 .../spreadsheets/custom-spreadsheet.ts        | 45 ++++++++++++++++---
 .../reports/spreadsheets/filterEmptyRows.ts   |  7 ++-
 .../spreadsheets/grouped-spreadsheet.ts       | 25 ++++++++++-
 .../reports/spreadsheets/recalculate.ts       | 14 ++++--
 .../loot-core/src/types/models/reports.d.ts   | 13 ++++++
 upcoming-release-notes/2871.md                |  6 +++
 22 files changed, 210 insertions(+), 52 deletions(-)
 create mode 100644 upcoming-release-notes/2871.md

diff --git a/packages/desktop-client/src/components/reports/ReportOptions.ts b/packages/desktop-client/src/components/reports/ReportOptions.ts
index 25fc25551..6fe42586b 100644
--- a/packages/desktop-client/src/components/reports/ReportOptions.ts
+++ b/packages/desktop-client/src/components/reports/ReportOptions.ts
@@ -35,6 +35,8 @@ const balanceTypeOptions = [
   { description: 'Payment', format: 'totalDebts' as const },
   { description: 'Deposit', format: 'totalAssets' as const },
   { description: 'Net', format: 'totalTotals' as const },
+  { description: 'Net Payment', format: 'netDebts' as const },
+  { description: 'Net Deposit', format: 'netAssets' as const },
 ];
 
 const groupByOptions = [
diff --git a/packages/desktop-client/src/components/reports/ReportSummary.tsx b/packages/desktop-client/src/components/reports/ReportSummary.tsx
index 9e25d548c..88968e67a 100644
--- a/packages/desktop-client/src/components/reports/ReportSummary.tsx
+++ b/packages/desktop-client/src/components/reports/ReportSummary.tsx
@@ -6,7 +6,10 @@ import {
   integerToCurrency,
   amountToInteger,
 } from 'loot-core/src/shared/util';
-import { type DataEntity } from 'loot-core/src/types/models/reports';
+import {
+  type balanceTypeOpType,
+  type DataEntity,
+} from 'loot-core/src/types/models/reports';
 
 import { theme, styles } from '../../style';
 import { Text } from '../common/Text';
@@ -19,7 +22,7 @@ type ReportSummaryProps = {
   startDate: string;
   endDate: string;
   data: DataEntity;
-  balanceTypeOp: 'totalDebts' | 'totalAssets' | 'totalTotals';
+  balanceTypeOp: balanceTypeOpType;
   interval: string;
   intervalsCount: number;
 };
@@ -33,9 +36,13 @@ export function ReportSummary({
   intervalsCount,
 }: ReportSummaryProps) {
   const net =
-    Math.abs(data.totalDebts) > Math.abs(data.totalAssets)
-      ? 'PAYMENT'
-      : 'DEPOSIT';
+    balanceTypeOp === 'netAssets'
+      ? 'DEPOSIT'
+      : balanceTypeOp === 'netDebts'
+        ? 'PAYMENT'
+        : Math.abs(data.totalDebts) > Math.abs(data.totalAssets)
+          ? 'PAYMENT'
+          : 'DEPOSIT';
   const average = amountToInteger(data[balanceTypeOp]) / intervalsCount;
   return (
     <View
diff --git a/packages/desktop-client/src/components/reports/disabledList.ts b/packages/desktop-client/src/components/reports/disabledList.ts
index eb3e70971..4042ad77a 100644
--- a/packages/desktop-client/src/components/reports/disabledList.ts
+++ b/packages/desktop-client/src/components/reports/disabledList.ts
@@ -63,7 +63,7 @@ const totalGraphOptions: graphOptions[] = [
     description: 'BarGraph',
     disabledSplit: [],
     defaultSplit: 'Category',
-    disabledType: ['Net'],
+    disabledType: [],
     defaultType: 'Payment',
   },
   {
@@ -88,7 +88,7 @@ const timeGraphOptions: graphOptions[] = [
     description: 'TableGraph',
     disabledSplit: ['Interval'],
     defaultSplit: 'Category',
-    disabledType: [],
+    disabledType: ['Net Payment', 'Net Deposit'],
     defaultType: 'Payment',
     disableLegend: true,
     disableLabel: true,
@@ -97,14 +97,14 @@ const timeGraphOptions: graphOptions[] = [
     description: 'StackedBarGraph',
     disabledSplit: ['Interval'],
     defaultSplit: 'Category',
-    disabledType: ['Net'],
+    disabledType: [],
     defaultType: 'Payment',
   },
   {
     description: 'LineGraph',
     disabledSplit: ['Interval'],
     defaultSplit: 'Category',
-    disabledType: ['Net'],
+    disabledType: [],
     defaultType: 'Payment',
     disableLegend: false,
     disableLabel: true,
diff --git a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx
index 03a5934b3..60c315d2a 100644
--- a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx
@@ -16,7 +16,10 @@ import {
   amountToCurrency,
   amountToCurrencyNoDecimal,
 } from 'loot-core/src/shared/util';
-import { type DataEntity } from 'loot-core/src/types/models/reports';
+import {
+  type balanceTypeOpType,
+  type DataEntity,
+} from 'loot-core/src/types/models/reports';
 
 import { usePrivacyMode } from '../../../hooks/usePrivacyMode';
 import { useResponsive } from '../../../ResponsiveProvider';
@@ -33,6 +36,8 @@ type PayloadItem = {
     date: string;
     totalAssets: number | string;
     totalDebts: number | string;
+    netAssets: number | string;
+    netDebts: number | string;
     totalTotals: number | string;
   };
 };
@@ -40,7 +45,7 @@ type PayloadItem = {
 type CustomTooltipProps = {
   active?: boolean;
   payload?: PayloadItem[];
-  balanceTypeOp: 'totalAssets' | 'totalTotals' | 'totalDebts';
+  balanceTypeOp: balanceTypeOpType;
 };
 
 const CustomTooltip = ({
@@ -74,10 +79,22 @@ const CustomTooltip = ({
             )}
             {['totalDebts', 'totalTotals'].includes(balanceTypeOp) && (
               <AlignedText
-                left="Debt:"
+                left="Debts:"
                 right={amountToCurrency(payload[0].payload.totalDebts)}
               />
             )}
+            {['netAssets'].includes(balanceTypeOp) && (
+              <AlignedText
+                left="Net Assets:"
+                right={amountToCurrency(payload[0].payload.netAssets)}
+              />
+            )}
+            {['netDebts'].includes(balanceTypeOp) && (
+              <AlignedText
+                left="Net Debts:"
+                right={amountToCurrency(payload[0].payload.netDebts)}
+              />
+            )}
             {['totalTotals'].includes(balanceTypeOp) && (
               <AlignedText
                 left="Net:"
@@ -132,7 +149,7 @@ const customLabel = ({
 type AreaGraphProps = {
   style?: CSSProperties;
   data: DataEntity;
-  balanceTypeOp: 'totalAssets' | 'totalTotals' | 'totalDebts';
+  balanceTypeOp: balanceTypeOpType;
   compact?: boolean;
   viewLabels: boolean;
 };
diff --git a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
index e6f1a0a6e..37ca3cef8 100644
--- a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
@@ -19,7 +19,10 @@ import {
   amountToCurrency,
   amountToCurrencyNoDecimal,
 } from 'loot-core/src/shared/util';
-import { type DataEntity } from 'loot-core/src/types/models/reports';
+import {
+  type balanceTypeOpType,
+  type DataEntity,
+} from 'loot-core/src/types/models/reports';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { useAccounts } from '../../../hooks/useAccounts';
@@ -50,6 +53,8 @@ type PayloadItem = {
     name: string;
     totalAssets: number | string;
     totalDebts: number | string;
+    netAssets: number | string;
+    netDebts: number | string;
     totalTotals: number | string;
     networth: number | string;
     totalChange: number | string;
@@ -60,7 +65,7 @@ type PayloadItem = {
 type CustomTooltipProps = {
   active?: boolean;
   payload?: PayloadItem[];
-  balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals';
+  balanceTypeOp?: balanceTypeOpType;
   yAxis?: string;
 };
 
@@ -96,10 +101,22 @@ const CustomTooltip = ({
             )}
             {['totalDebts', 'totalTotals'].includes(balanceTypeOp) && (
               <AlignedText
-                left="Debt:"
+                left="Debts:"
                 right={amountToCurrency(payload[0].payload.totalDebts)}
               />
             )}
+            {['netAssets'].includes(balanceTypeOp) && (
+              <AlignedText
+                left="Net Assets:"
+                right={amountToCurrency(payload[0].payload.netAssets)}
+              />
+            )}
+            {['netDebts'].includes(balanceTypeOp) && (
+              <AlignedText
+                left="Net Debts:"
+                right={amountToCurrency(payload[0].payload.netDebts)}
+              />
+            )}
             {['totalTotals'].includes(balanceTypeOp) && (
               <AlignedText
                 left="Net:"
@@ -137,7 +154,7 @@ type BarGraphProps = {
   data: DataEntity;
   filters: RuleConditionEntity[];
   groupBy: string;
-  balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
+  balanceTypeOp: balanceTypeOpType;
   compact?: boolean;
   viewLabels: boolean;
   showHiddenCategories?: boolean;
@@ -167,11 +184,15 @@ export function BarGraph({
   const labelsMargin = viewLabels ? 30 : 0;
 
   const getVal = obj => {
-    if (balanceTypeOp === 'totalDebts') {
-      return -1 * obj.totalDebts;
-    } else {
+    if (balanceTypeOp === 'totalTotals' && groupBy === 'Interval') {
       return obj.totalAssets;
     }
+
+    if (['totalDebts', 'netDebts'].includes(balanceTypeOp)) {
+      return -1 * obj[balanceTypeOp];
+    }
+
+    return obj[balanceTypeOp];
   };
 
   const longestLabelLength = data[splitData]
diff --git a/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx b/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx
index 3eab0f182..d6b02490b 100644
--- a/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx
@@ -4,7 +4,10 @@ import React, { useState } from 'react';
 import { PieChart, Pie, Cell, Sector, ResponsiveContainer } from 'recharts';
 
 import { amountToCurrency } from 'loot-core/src/shared/util';
-import { type DataEntity } from 'loot-core/src/types/models/reports';
+import {
+  type balanceTypeOpType,
+  type DataEntity,
+} from 'loot-core/src/types/models/reports';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { useAccounts } from '../../../hooks/useAccounts';
@@ -181,7 +184,7 @@ type DonutGraphProps = {
   data: DataEntity;
   filters: RuleConditionEntity[];
   groupBy: string;
-  balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
+  balanceTypeOp: balanceTypeOpType;
   compact?: boolean;
   viewLabels: boolean;
   showHiddenCategories?: boolean;
@@ -209,7 +212,7 @@ export function DonutGraph({
   const [pointer, setPointer] = useState('');
 
   const getVal = obj => {
-    if (balanceTypeOp === 'totalDebts') {
+    if (['totalDebts', 'netDebts'].includes(balanceTypeOp)) {
       return -1 * obj[balanceTypeOp];
     } else {
       return obj[balanceTypeOp];
diff --git a/packages/desktop-client/src/components/reports/graphs/LineGraph.tsx b/packages/desktop-client/src/components/reports/graphs/LineGraph.tsx
index d47575f88..14c53ad28 100644
--- a/packages/desktop-client/src/components/reports/graphs/LineGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/LineGraph.tsx
@@ -16,7 +16,10 @@ import {
   amountToCurrency,
   amountToCurrencyNoDecimal,
 } from 'loot-core/src/shared/util';
-import { type DataEntity } from 'loot-core/types/models/reports';
+import {
+  type balanceTypeOpType,
+  type DataEntity,
+} from 'loot-core/types/models/reports';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { useAccounts } from '../../../hooks/useAccounts';
@@ -115,7 +118,7 @@ type LineGraphProps = {
   filters: RuleConditionEntity[];
   groupBy: string;
   compact?: boolean;
-  balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
+  balanceTypeOp: balanceTypeOpType;
   showHiddenCategories?: boolean;
   showOffBudget?: boolean;
   interval?: string;
diff --git a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx
index 81463d147..309fa8618 100644
--- a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx
@@ -17,7 +17,10 @@ import {
   amountToCurrency,
   amountToCurrencyNoDecimal,
 } from 'loot-core/src/shared/util';
-import { type DataEntity } from 'loot-core/src/types/models/reports';
+import {
+  type balanceTypeOpType,
+  type DataEntity,
+} from 'loot-core/src/types/models/reports';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { useAccounts } from '../../../hooks/useAccounts';
@@ -144,7 +147,7 @@ type StackedBarGraphProps = {
   groupBy: string;
   compact?: boolean;
   viewLabels: boolean;
-  balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
+  balanceTypeOp: balanceTypeOpType;
   showHiddenCategories?: boolean;
   showOffBudget?: boolean;
   interval?: string;
@@ -194,6 +197,7 @@ export function StackedBarGraph({
                 data={data.intervalData}
                 margin={{ top: 0, right: 0, left: leftMargin, bottom: 10 }}
                 style={{ cursor: pointer }}
+                stackOffset="sign" //stacked by sign
               >
                 {(!isNarrowWidth || !compact) && (
                   <Tooltip
diff --git a/packages/desktop-client/src/components/reports/graphs/showActivity.ts b/packages/desktop-client/src/components/reports/graphs/showActivity.ts
index 892664b81..085cc806d 100644
--- a/packages/desktop-client/src/components/reports/graphs/showActivity.ts
+++ b/packages/desktop-client/src/components/reports/graphs/showActivity.ts
@@ -4,6 +4,7 @@ import * as monthUtils from 'loot-core/src/shared/months';
 import { type AccountEntity } from 'loot-core/types/models/account';
 import { type CategoryEntity } from 'loot-core/types/models/category';
 import { type CategoryGroupEntity } from 'loot-core/types/models/category-group';
+import { type balanceTypeOpType } from 'loot-core/types/models/reports';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { ReportOptions } from '../ReportOptions';
@@ -12,7 +13,7 @@ type showActivityProps = {
   navigate: NavigateFunction;
   categories: { list: CategoryEntity[]; grouped: CategoryGroupEntity[] };
   accounts: AccountEntity[];
-  balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
+  balanceTypeOp: balanceTypeOpType;
   filters: RuleConditionEntity[];
   showHiddenCategories: boolean;
   showOffBudget: boolean;
@@ -50,7 +51,7 @@ export function showActivity({
           'FromDate') as 'dayFromDate' | 'monthFromDate' | 'yearFromDate');
   const isDateOp = interval === 'Weekly' || type !== 'time';
 
-  const conditions = [
+  const filterConditions = [
     ...filters,
     id && { field, op: 'is', value: id, type: 'id' },
     {
@@ -66,8 +67,9 @@ export function showActivity({
       options: { date: true },
     },
     !(
-      balanceTypeOp === 'totalTotals' &&
-      (type === 'totals' || type === 'time')
+      ['netAssets', 'netDebts'].includes(balanceTypeOp) ||
+      (balanceTypeOp === 'totalTotals' &&
+        (type === 'totals' || type === 'time'))
     ) && {
       field: 'amount',
       op: 'gte',
@@ -96,7 +98,7 @@ export function showActivity({
   navigate('/accounts', {
     state: {
       goBack: true,
-      conditions,
+      filterConditions,
     },
   });
 }
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx
index 0da32fb5c..6db7986aa 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx
@@ -9,6 +9,7 @@ import React, {
 import {
   type GroupedEntity,
   type DataEntity,
+  type balanceTypeOpType,
 } from 'loot-core/src/types/models/reports';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
@@ -28,7 +29,7 @@ type ReportTableProps = {
   totalScrollRef: RefObject<HTMLDivElement>;
   handleScroll: UIEventHandler<HTMLDivElement>;
   groupBy: string;
-  balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets';
+  balanceTypeOp: balanceTypeOpType;
   data: DataEntity;
   filters?: RuleConditionEntity[];
   mode: string;
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx
index 2dea557de..926beef7e 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx
@@ -1,6 +1,9 @@
 import React, { type RefObject, type UIEventHandler } from 'react';
 
-import { type IntervalEntity } from 'loot-core/src/types/models/reports';
+import {
+  type balanceTypeOpType,
+  type IntervalEntity,
+} from 'loot-core/src/types/models/reports';
 
 import { theme } from '../../../../style';
 import { type CSSProperties } from '../../../../style/types';
@@ -12,7 +15,7 @@ type ReportTableHeaderProps = {
   groupBy: string;
   interval: string;
   data: IntervalEntity[];
-  balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets';
+  balanceTypeOp: balanceTypeOpType;
   headerScrollRef: RefObject<HTMLDivElement>;
   handleScroll: UIEventHandler<HTMLDivElement>;
   compact: boolean;
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx
index f3ba6bad8..e26b51367 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx
@@ -38,6 +38,8 @@ export function ReportTableList({
               date: interval.date,
               totalAssets: interval.totalAssets,
               totalDebts: interval.totalDebts,
+              netAssets: interval.netAssets,
+              netDebts: interval.netDebts,
               totalTotals: interval.totalTotals,
               intervalData: [],
               categories: [],
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx
index c18cf54c9..11a8dea35 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx
@@ -5,7 +5,10 @@ import {
   amountToInteger,
   integerToCurrency,
 } from 'loot-core/src/shared/util';
-import { type GroupedEntity } from 'loot-core/types/models/reports';
+import {
+  type balanceTypeOpType,
+  type GroupedEntity,
+} from 'loot-core/types/models/reports';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { useAccounts } from '../../../../hooks/useAccounts';
@@ -20,7 +23,7 @@ import { showActivity } from '../showActivity';
 
 type ReportTableRowProps = {
   item: GroupedEntity;
-  balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
+  balanceTypeOp: balanceTypeOpType;
   groupBy: string;
   mode: string;
   filters?: RuleConditionEntity[];
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx
index 8690fc709..ed192f0c4 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx
@@ -85,6 +85,8 @@ export function ReportTableTotals({
     intervalData: data.intervalData,
     totalAssets: data.totalAssets,
     totalDebts: data.totalDebts,
+    netAssets: data.netAssets,
+    netDebts: data.netDebts,
     totalTotals: data.totalTotals,
   };
 
diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx
index 2fb2a375c..edeba3fbf 100644
--- a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx
+++ b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx
@@ -8,6 +8,7 @@ import * as monthUtils from 'loot-core/src/shared/months';
 import { amountToCurrency } from 'loot-core/src/shared/util';
 import { type CategoryEntity } from 'loot-core/types/models/category';
 import {
+  type balanceTypeOpType,
   type CustomReportEntity,
   type DataEntity,
 } from 'loot-core/types/models/reports';
@@ -248,7 +249,7 @@ export function CustomReport() {
     }
   }, [interval, startDate, endDate, firstDayOfWeekIdx]);
 
-  const balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals' =
+  const balanceTypeOp: balanceTypeOpType =
     ReportOptions.balanceTypeMap.get(balanceType) || 'totalDebts';
   const payees = usePayees();
   const accounts = useAccounts();
@@ -692,7 +693,7 @@ export function CustomReport() {
                       right={
                         <Text>
                           <PrivacyFilter blurIntensity={5}>
-                            {amountToCurrency(Math.abs(data[balanceTypeOp]))}
+                            {amountToCurrency(data[balanceTypeOp])}
                           </PrivacyFilter>
                         </Text>
                       }
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/calculateLegend.ts b/packages/desktop-client/src/components/reports/spreadsheets/calculateLegend.ts
index 7ada7dec8..93a80014f 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/calculateLegend.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/calculateLegend.ts
@@ -2,6 +2,7 @@ import {
   type LegendEntity,
   type IntervalEntity,
   type GroupedEntity,
+  type balanceTypeOpType,
 } from 'loot-core/src/types/models/reports';
 
 import { theme } from '../../../style';
@@ -12,7 +13,7 @@ export function calculateLegend(
   calcDataFiltered: GroupedEntity[],
   groupBy: string,
   graphType?: string,
-  balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals',
+  balanceTypeOp?: balanceTypeOpType,
 ): LegendEntity[] {
   const colorScale = getColorScale('qualitative');
   const chooseData =
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts
index 0ad83924b..86a2fd6eb 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts
@@ -13,6 +13,7 @@ import {
   type CategoryGroupEntity,
 } from 'loot-core/src/types/models';
 import {
+  type balanceTypeOpType,
   type DataEntity,
   type GroupedEntity,
   type IntervalEntity,
@@ -46,7 +47,7 @@ export type createCustomSpreadsheetProps = {
   showHiddenCategories: boolean;
   showUncategorized: boolean;
   groupBy?: string;
-  balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals';
+  balanceTypeOp?: balanceTypeOpType;
   payees?: PayeeEntity[];
   accounts?: AccountEntity[];
   graphType?: string;
@@ -153,11 +154,16 @@ export function createCustomSpreadsheet({
 
     let totalAssets = 0;
     let totalDebts = 0;
+    let netAssets = 0;
+    let netDebts = 0;
 
     const intervalData = intervals.reduce(
       (arr: IntervalEntity[], intervalItem, index) => {
         let perIntervalAssets = 0;
         let perIntervalDebts = 0;
+        let perIntervalNetAssets = 0;
+        let perIntervalNetDebts = 0;
+        let perIntervalTotals = 0;
         const stacked: Record<string, number> = {};
 
         groupByList.map(item => {
@@ -193,20 +199,43 @@ export function createCustomSpreadsheet({
             .reduce((a, v) => (a = a + v.amount), 0);
           perIntervalDebts += intervalDebts;
 
+          const netAmounts = intervalAssets + intervalDebts;
+
           if (balanceTypeOp === 'totalAssets') {
             stackAmounts += intervalAssets;
           }
           if (balanceTypeOp === 'totalDebts') {
-            stackAmounts += intervalDebts;
+            stackAmounts += Math.abs(intervalDebts);
+          }
+          if (balanceTypeOp === 'netAssets') {
+            stackAmounts += netAmounts > 0 ? netAmounts : 0;
+          }
+          if (balanceTypeOp === 'netDebts') {
+            stackAmounts = netAmounts < 0 ? Math.abs(netAmounts) : 0;
+          }
+          if (balanceTypeOp === 'totalTotals') {
+            stackAmounts += netAmounts;
           }
           if (stackAmounts !== 0) {
-            stacked[item.name] = integerToAmount(Math.abs(stackAmounts));
+            stacked[item.name] = integerToAmount(stackAmounts);
           }
 
+          perIntervalNetAssets =
+            netAmounts > 0
+              ? perIntervalNetAssets + netAmounts
+              : perIntervalNetAssets;
+          perIntervalNetDebts =
+            netAmounts < 0
+              ? perIntervalNetDebts + netAmounts
+              : perIntervalNetDebts;
+          perIntervalTotals += netAmounts;
+
           return null;
         });
         totalAssets += perIntervalAssets;
         totalDebts += perIntervalDebts;
+        netAssets += perIntervalNetAssets;
+        netDebts += perIntervalNetDebts;
 
         arr.push({
           date: d.format(
@@ -219,9 +248,11 @@ export function createCustomSpreadsheet({
             index + 1 === intervals.length
               ? endDate
               : monthUtils.subDays(intervals[index + 1], 1),
-          totalDebts: integerToAmount(perIntervalDebts),
           totalAssets: integerToAmount(perIntervalAssets),
-          totalTotals: integerToAmount(perIntervalDebts + perIntervalAssets),
+          totalDebts: integerToAmount(perIntervalDebts),
+          netAssets: integerToAmount(perIntervalNetAssets),
+          netDebts: integerToAmount(perIntervalNetDebts),
+          totalTotals: integerToAmount(perIntervalTotals),
         });
 
         return arr;
@@ -262,8 +293,10 @@ export function createCustomSpreadsheet({
       legend,
       startDate,
       endDate,
-      totalDebts: integerToAmount(totalDebts),
       totalAssets: integerToAmount(totalAssets),
+      totalDebts: integerToAmount(totalDebts),
+      netAssets: integerToAmount(netAssets),
+      netDebts: integerToAmount(netDebts),
       totalTotals: integerToAmount(totalAssets + totalDebts),
     });
     setDataCheck?.(true);
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts b/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts
index b133a67da..922696e9c 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts
@@ -1,4 +1,7 @@
-import { type GroupedEntity } from 'loot-core/src/types/models/reports';
+import {
+  type balanceTypeOpType,
+  type GroupedEntity,
+} from 'loot-core/src/types/models/reports';
 
 export function filterEmptyRows({
   showEmpty,
@@ -7,7 +10,7 @@ export function filterEmptyRows({
 }: {
   showEmpty: boolean;
   data: GroupedEntity;
-  balanceTypeOp?: 'totalAssets' | 'totalDebts' | 'totalTotals';
+  balanceTypeOp?: balanceTypeOpType;
 }): boolean {
   let showHide: boolean;
   if (balanceTypeOp === 'totalTotals') {
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts
index ed3048d29..414bb93a8 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts
@@ -111,11 +111,16 @@ export function createGroupedSpreadsheet({
       group => {
         let totalAssets = 0;
         let totalDebts = 0;
+        let netAssets = 0;
+        let netDebts = 0;
 
         const intervalData = intervals.reduce(
           (arr: IntervalEntity[], intervalItem) => {
             let groupedAssets = 0;
             let groupedDebts = 0;
+            let groupedNetAssets = 0;
+            let groupedNetDebts = 0;
+            let groupedTotals = 0;
 
             if (!group.categories) {
               return [];
@@ -151,16 +156,32 @@ export function createGroupedSpreadsheet({
                 )
                 .reduce((a, v) => (a = a + v.amount), 0);
               groupedDebts += intervalDebts;
+
+              const intervalTotals = intervalAssets + intervalDebts;
+
+              groupedNetAssets =
+                intervalTotals > 0
+                  ? groupedNetAssets + intervalTotals
+                  : groupedNetAssets;
+              groupedNetDebts =
+                intervalTotals < 0
+                  ? groupedNetDebts + intervalTotals
+                  : groupedNetDebts;
+              groupedTotals += intervalTotals;
             });
 
             totalAssets += groupedAssets;
             totalDebts += groupedDebts;
+            netAssets += groupedNetAssets;
+            netDebts += groupedNetDebts;
 
             arr.push({
               date: intervalItem,
               totalAssets: integerToAmount(groupedAssets),
               totalDebts: integerToAmount(groupedDebts),
-              totalTotals: integerToAmount(groupedDebts + groupedAssets),
+              netAssets: integerToAmount(groupedNetAssets),
+              netDebts: integerToAmount(groupedNetDebts),
+              totalTotals: integerToAmount(groupedTotals),
             });
 
             return arr;
@@ -191,6 +212,8 @@ export function createGroupedSpreadsheet({
           name: group.name,
           totalAssets: integerToAmount(totalAssets),
           totalDebts: integerToAmount(totalDebts),
+          netAssets: integerToAmount(netAssets),
+          netDebts: integerToAmount(netDebts),
           totalTotals: integerToAmount(totalAssets + totalDebts),
           intervalData,
           categories:
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts b/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts
index 3f4bc44b7..d0dc1e1c9 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts
@@ -73,14 +73,18 @@ export function recalculate({
         .reduce((a, v) => (a = a + v.amount), 0);
       totalDebts += intervalDebts;
 
+      const intervalTotals = intervalAssets + intervalDebts;
+
       const change = last
-        ? intervalAssets + intervalDebts - amountToInteger(last.totalTotals)
+        ? intervalTotals - amountToInteger(last.totalTotals)
         : 0;
 
       arr.push({
         totalAssets: integerToAmount(intervalAssets),
         totalDebts: integerToAmount(intervalDebts),
-        totalTotals: integerToAmount(intervalAssets + intervalDebts),
+        netAssets: intervalTotals > 0 ? integerToAmount(intervalTotals) : 0,
+        netDebts: intervalTotals < 0 ? integerToAmount(intervalTotals) : 0,
+        totalTotals: integerToAmount(intervalTotals),
         change,
         intervalStartDate: index === 0 ? startDate : intervalItem,
         intervalEndDate:
@@ -94,12 +98,16 @@ export function recalculate({
     [],
   );
 
+  const totalTotals = totalAssets + totalDebts;
+
   return {
     id: item.id || '',
     name: item.name,
     totalAssets: integerToAmount(totalAssets),
     totalDebts: integerToAmount(totalDebts),
-    totalTotals: integerToAmount(totalAssets + totalDebts),
+    netAssets: totalTotals > 0 ? integerToAmount(totalTotals) : 0,
+    netDebts: totalTotals < 0 ? integerToAmount(totalTotals) : 0,
+    totalTotals: integerToAmount(totalTotals),
     intervalData,
   };
 }
diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts
index d7e04a5f8..da6452786 100644
--- a/packages/loot-core/src/types/models/reports.d.ts
+++ b/packages/loot-core/src/types/models/reports.d.ts
@@ -25,6 +25,13 @@ export interface CustomReportEntity {
   tombstone?: boolean;
 }
 
+export type balanceTypeOpType =
+  | 'totalAssets'
+  | 'totalDebts'
+  | 'totalTotals'
+  | 'netAssets'
+  | 'netDebts';
+
 export type SpendingMonthEntity = Record<
   string | number,
   {
@@ -68,6 +75,8 @@ export interface DataEntity {
   endDate?: string;
   totalDebts: number;
   totalAssets: number;
+  netAssets: number;
+  netDebts: number;
   totalTotals: number;
 }
 
@@ -84,6 +93,8 @@ export type IntervalEntity = {
   intervalEndDate?: string;
   totalAssets: number;
   totalDebts: number;
+  netAssets: number;
+  netDebts: number;
   totalTotals: number;
 };
 
@@ -95,6 +106,8 @@ export interface GroupedEntity {
   totalAssets: number;
   totalDebts: number;
   totalTotals: number;
+  netAssets: number;
+  netDebts: number;
   categories?: GroupedEntity[];
 }
 
diff --git a/upcoming-release-notes/2871.md b/upcoming-release-notes/2871.md
new file mode 100644
index 000000000..648846126
--- /dev/null
+++ b/upcoming-release-notes/2871.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [carkom]
+---
+
+Custom reports - rework "net" numbers to work more intuitively and allow for greater customization
-- 
GitLab