owlplanner 2025.12.20__py3-none-any.whl → 2026.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
owlplanner/data/rates.csv CHANGED
@@ -1,98 +1,99 @@
1
- year,S&P 500,Bonds Baa,Bonds Aaa,TBills,TNotes,Inflation
2
- 1928,43.81,3.22,3.22,3.08,0.84,-1.16
3
- 1929,-8.3,3.02,3.02,3.16,4.2,0.58
4
- 1930,-25.12,0.54,0.54,4.55,4.54,-6.4
5
- 1931,-43.84,-15.68,-15.68,2.31,-2.56,-9.32
6
- 1932,-8.64,23.59,23.59,1.07,8.79,-10.27
7
- 1933,49.98,12.97,12.97,0.96,1.86,0.76
8
- 1934,-1.19,18.82,18.82,0.28,7.96,1.52
9
- 1935,46.74,13.31,13.31,0.17,4.47,2.99
10
- 1936,31.94,11.38,11.38,0.17,5.02,1.45
11
- 1937,-35.34,-4.42,-4.42,0.28,1.38,2.86
12
- 1938,29.28,9.24,9.24,0.07,4.21,-2.78
13
- 1939,-1.1,7.98,7.98,0.05,4.41,0.0
14
- 1940,-10.67,8.65,8.65,0.04,5.4,0.71
15
- 1941,-12.77,5.01,5.01,0.13,-2.02,9.93
16
- 1942,19.17,5.18,5.18,0.34,2.29,9.03
17
- 1943,25.06,8.04,8.04,0.38,2.49,2.96
18
- 1944,19.03,6.57,6.57,0.38,2.58,2.3
19
- 1945,35.82,6.8,6.8,0.38,3.8,2.25
20
- 1946,-8.43,2.51,2.51,0.38,3.13,18.13
21
- 1947,5.2,0.26,0.26,0.6,0.92,8.84
22
- 1948,5.7,3.44,3.44,1.05,1.95,2.99
23
- 1949,18.3,5.38,5.38,1.12,4.66,-2.07
24
- 1950,30.81,4.24,4.24,1.2,0.43,5.93
25
- 1951,23.68,-0.19,-0.19,1.52,-0.3,6.0
26
- 1952,18.15,4.44,4.44,1.72,2.27,0.75
27
- 1953,-1.21,1.62,1.62,1.89,4.14,0.75
28
- 1954,52.56,6.16,6.16,0.94,3.29,-0.74
29
- 1955,32.6,2.04,2.04,1.72,-1.34,0.37
30
- 1956,7.44,-2.35,-2.35,2.62,-2.26,2.99
31
- 1957,-10.46,-0.72,-0.72,3.22,6.8,2.9
32
- 1958,43.72,6.43,6.43,1.77,-2.1,1.76
33
- 1959,12.06,1.57,1.57,3.39,-2.65,1.73
34
- 1960,0.34,6.66,6.66,2.87,11.64,1.36
35
- 1961,26.64,5.1,5.1,2.35,2.06,0.67
36
- 1962,-8.81,6.5,6.5,2.77,5.69,1.33
37
- 1963,22.61,5.46,5.46,3.16,1.68,1.64
38
- 1964,16.42,5.16,5.16,3.55,3.73,0.97
39
- 1965,12.4,3.19,3.19,3.95,0.72,1.92
40
- 1966,-9.97,-3.45,-3.45,4.86,2.91,3.46
41
- 1967,23.8,0.9,0.9,4.29,-1.58,3.04
42
- 1968,10.81,4.85,4.85,5.34,3.27,4.72
43
- 1969,-8.24,-2.03,-2.03,6.67,-5.01,6.2
44
- 1970,3.56,5.65,5.65,6.39,16.75,5.57
45
- 1971,14.22,14.0,14.0,4.33,9.79,3.27
46
- 1972,18.76,11.41,11.41,4.06,2.82,3.41
47
- 1973,-14.31,4.32,4.32,7.04,3.66,8.71
48
- 1974,-25.9,-4.38,-4.38,7.85,1.99,12.34
49
- 1975,37.0,11.05,11.05,5.79,3.61,6.94
50
- 1976,23.83,19.75,19.75,4.98,15.98,4.86
51
- 1977,-6.98,9.95,9.95,5.26,1.29,6.7
52
- 1978,6.51,3.14,3.14,7.18,-0.78,9.02
53
- 1979,18.52,-2.01,-2.01,10.05,0.67,13.29
54
- 1980,31.74,-3.32,-3.32,11.39,-2.99,12.52
55
- 1981,-4.7,8.46,8.46,14.04,8.2,8.92
56
- 1982,20.42,29.05,29.05,10.6,32.81,3.83
57
- 1983,22.34,16.19,16.19,8.62,3.2,3.79
58
- 1984,6.15,15.62,15.62,9.54,13.73,3.95
59
- 1985,31.24,23.86,23.86,7.47,25.71,3.8
60
- 1986,18.49,21.35,21.35,5.97,24.28,1.1
61
- 1987,5.81,2.81,2.81,5.78,-4.96,4.43
62
- 1988,16.54,14.38,14.38,6.67,8.22,4.42
63
- 1989,31.48,15.95,15.95,8.11,17.69,4.65
64
- 1990,-3.06,6.28,6.28,7.5,6.24,6.11
65
- 1991,30.23,18.93,18.93,5.38,15.0,3.06
66
- 1992,7.49,11.31,11.31,3.43,9.36,2.9
67
- 1993,9.97,15.47,15.47,3.0,14.21,2.75
68
- 1994,1.33,-0.97,-0.97,4.25,-8.04,2.67
69
- 1995,37.2,21.29,21.29,5.49,23.48,2.54
70
- 1996,22.68,3.42,3.42,5.01,1.43,3.32
71
- 1997,33.1,12.75,12.75,5.06,9.94,1.7
72
- 1998,28.34,7.63,7.63,4.78,14.92,1.61
73
- 1999,20.89,0.91,0.91,4.64,-8.25,2.68
74
- 2000,-9.03,9.39,9.39,5.82,16.66,3.39
75
- 2001,-11.85,8.54,8.54,3.4,5.57,1.55
76
- 2002,-21.97,12.14,12.14,1.61,15.12,2.38
77
- 2003,28.36,12.32,12.32,1.01,0.38,1.88
78
- 2004,10.74,10.35,10.35,1.37,4.49,3.26
79
- 2005,4.83,5.3,5.3,3.15,2.87,3.42
80
- 2006,15.61,5.2,5.2,4.73,1.96,2.54
81
- 2007,5.48,4.84,4.84,4.36,10.21,4.08
82
- 2008,-36.55,-3.54,-3.54,1.37,20.1,0.09
83
- 2009,25.94,20.21,20.21,0.15,-11.12,2.72
84
- 2010,14.82,9.41,9.41,0.14,8.46,1.5
85
- 2011,2.1,12.26,12.26,0.05,16.04,2.96
86
- 2012,15.89,9.33,9.33,0.09,2.97,1.74
87
- 2013,32.15,-0.98,-0.98,0.06,-9.1,1.5
88
- 2014,13.52,10.78,10.78,0.03,10.75,0.76
89
- 2015,1.38,-1.5,-1.5,0.05,1.28,0.73
90
- 2016,11.77,11.52,11.52,0.32,0.69,2.07
91
- 2017,21.61,9.23,9.23,0.93,2.8,2.11
92
- 2018,-4.23,-3.27,-3.27,1.94,-0.02,1.91
93
- 2019,31.21,15.25,15.25,2.06,9.64,2.29
94
- 2020,18.02,10.6,10.6,0.35,11.33,1.36
95
- 2021,28.47,0.93,0.93,0.05,-4.42,7.04
96
- 2022,-18.04,-15.14,-15.14,2.02,-17.83,6.45
97
- 2023,26.06,8.74,8.74,5.07,3.88,3.35
98
- 2024,24.88,1.74,1.74,4.97,-1.64,2.75
1
+ year,S&P 500,Bonds Baa,real estate,TBills,TNotes,Inflation
2
+ 1928,43.81,3.22,62.15,3.08,0.84,-1.16
3
+ 1929,-8.30,3.02,-46.08,3.16,4.20,0.58
4
+ 1930,-25.12,0.54,-48.35,4.55,4.54,-6.40
5
+ 1931,-43.84,-15.68,-43.62,2.31,-2.56,-9.32
6
+ 1932,-8.64,23.59,28.65,1.07,8.79,-10.27
7
+ 1933,49.98,12.97,146.60,0.96,1.86,0.76
8
+ 1934,-1.19,18.82,23.07,0.28,7.96,1.52
9
+ 1935,46.74,13.31,54.90,0.17,4.47,2.99
10
+ 1936,31.94,11.38,96.41,0.17,5.02,1.45
11
+ 1937,-35.34,-4.42,-53.94,0.28,1.38,2.86
12
+ 1938,29.28,9.24,5.16,0.06,4.21,-2.78
13
+ 1939,-1.10,7.98,-4.86,0.05,4.41,0.00
14
+ 1940,-10.67,8.65,-32.88,0.04,5.40,0.71
15
+ 1941,-12.77,5.01,-6.75,0.13,-2.02,9.93
16
+ 1942,19.17,5.18,63.01,0.34,2.29,9.03
17
+ 1943,25.06,8.04,143.02,0.38,2.49,2.96
18
+ 1944,19.03,6.57,71.15,0.38,2.58,2.30
19
+ 1945,35.82,6.80,94.41,0.38,3.80,2.25
20
+ 1946,-8.43,2.51,-13.73,0.38,3.13,18.13
21
+ 1947,5.20,0.26,-1.74,0.60,0.92,8.84
22
+ 1948,5.70,3.44,-0.01,1.04,1.95,2.99
23
+ 1949,18.30,5.38,27.60,1.12,4.66,-2.07
24
+ 1950,30.81,4.24,52.81,1.20,0.43,5.93
25
+ 1951,23.68,-0.19,3.87,1.52,-0.30,6.00
26
+ 1952,18.15,4.44,1.02,1.72,2.27,0.75
27
+ 1953,-1.21,1.62,-5.97,1.89,4.14,0.75
28
+ 1954,52.56,6.16,64.97,0.94,3.29,-0.74
29
+ 1955,32.60,2.04,26.72,1.72,-1.34,0.37
30
+ 1956,7.44,-2.35,-0.89,2.62,-2.26,2.99
31
+ 1957,-10.46,-0.72,-15.19,3.22,6.80,2.90
32
+ 1958,43.72,6.43,68.80,1.77,-2.10,1.76
33
+ 1959,12.06,1.57,12.70,3.39,-2.65,1.73
34
+ 1960,0.34,6.66,-3.57,2.87,11.64,1.36
35
+ 1961,26.64,5.10,29.45,2.35,2.06,0.67
36
+ 1962,-8.81,6.50,-9.78,2.77,5.69,1.33
37
+ 1963,22.61,5.46,19.65,3.16,1.68,1.64
38
+ 1964,16.42,5.16,23.25,3.55,3.73,0.97
39
+ 1965,12.40,3.19,45.24,3.95,0.72,1.92
40
+ 1966,-9.97,-3.45,-9.47,4.86,2.91,3.46
41
+ 1967,23.80,0.90,115.87,4.29,-1.58,3.04
42
+ 1968,10.81,4.85,60.69,5.34,3.27,4.72
43
+ 1969,-8.24,-2.03,-32.95,6.67,-5.01,6.20
44
+ 1970,3.56,5.65,-18.78,6.39,16.75,5.57
45
+ 1971,14.22,14.00,15.96,4.33,9.79,3.27
46
+ 1972,18.76,11.41,0.16,4.06,2.82,3.41
47
+ 1973,-14.31,4.32,-38.80,7.04,3.66,8.71
48
+ 1974,-25.90,-4.38,-26.90,7.85,1.99,12.34
49
+ 1975,37.00,11.05,59.68,5.79,3.61,6.94
50
+ 1976,23.83,19.75,48.62,4.98,15.98,4.86
51
+ 1977,-6.98,9.95,30.29,5.26,1.29,6.70
52
+ 1978,6.51,3.14,28.89,7.18,-0.78,9.02
53
+ 1979,18.52,-2.01,41.69,10.05,0.67,13.29
54
+ 1980,31.74,-3.32,41.92,11.39,-2.99,12.52
55
+ 1981,-4.70,8.46,-4.29,14.04,8.20,8.92
56
+ 1982,20.42,29.05,26.85,11.09,32.81,3.83
57
+ 1983,22.34,16.19,34.86,8.95,3.20,3.79
58
+ 1984,6.15,15.62,-14.50,9.92,13.73,3.95
59
+ 1985,31.24,23.86,24.51,7.72,25.71,3.80
60
+ 1986,18.49,22.15,2.09,6.15,24.28,1.10
61
+ 1987,5.81,1.12,-14.00,5.96,-4.96,4.43
62
+ 1988,16.54,15.68,17.15,6.89,8.22,4.42
63
+ 1989,31.48,16.31,6.96,8.39,17.69,4.65
64
+ 1990,-3.06,5.65,-27.77,7.75,6.24,6.11
65
+ 1991,30.23,16.40,46.07,5.54,15.00,3.06
66
+ 1992,7.49,13.68,25.34,3.51,9.36,2.90
67
+ 1993,9.97,16.44,25.56,3.07,14.21,2.75
68
+ 1994,1.33,-1.23,-4.76,4.37,-8.04,2.67
69
+ 1995,37.20,20.09,32.12,5.66,23.48,2.54
70
+ 1996,22.68,5.28,14.79,5.15,1.43,3.32
71
+ 1997,33.10,11.30,22.06,5.20,9.94,1.70
72
+ 1998,28.34,8.10,-13.47,4.91,14.92,1.61
73
+ 1999,20.89,0.97,37.71,4.78,-8.25,2.68
74
+ 2000,-9.03,9.38,-9.13,6.00,16.66,3.39
75
+ 2001,-11.85,8.60,32.14,3.48,5.57,1.55
76
+ 2002,-21.97,12.06,-3.61,1.64,15.12,2.38
77
+ 2003,28.36,12.38,91.23,1.03,0.38,1.88
78
+ 2004,10.74,10.33,17.30,1.40,4.49,3.26
79
+ 2005,4.83,5.13,3.78,3.22,2.87,3.42
80
+ 2006,15.61,5.27,18.40,4.85,1.96,2.54
81
+ 2007,5.48,4.90,-9.11,4.48,10.21,4.08
82
+ 2008,-36.55,-3.44,-44.68,1.40,20.10,0.09
83
+ 2009,25.94,19.96,46.94,0.15,-11.12,2.72
84
+ 2010,14.82,9.40,27.73,0.14,8.46,1.50
85
+ 2011,2.10,12.26,-14.04,0.05,16.04,2.96
86
+ 2012,15.89,9.40,18.96,0.09,2.97,1.74
87
+ 2013,32.15,-1.13,50.30,0.06,-9.10,1.50
88
+ 2014,13.52,10.75,1.53,0.03,10.75,0.76
89
+ 2015,1.38,-1.50,-9.12,0.05,1.28,0.73
90
+ 2016,11.77,11.52,17.02,0.32,0.69,2.07
91
+ 2017,21.61,9.15,15.13,0.95,2.80,2.11
92
+ 2018,-4.23,-3.18,-16.21,1.97,-0.02,1.91
93
+ 2019,31.21,15.25,11.92,2.11,9.64,2.29
94
+ 2020,18.02,10.60,34.16,0.36,11.33,1.36
95
+ 2021,28.47,1.02,22.41,0.04,-4.42,7.04
96
+ 2022,-18.04,-15.23,-22.90,2.09,-17.83,6.45
97
+ 2023,26.06,8.74,5.19,5.28,3.88,3.35
98
+ 2024,24.88,1.74,8.70,5.18,-1.64,2.89
99
+ 2025,17.78,6.96,16.53,4.21,8.29,2.74
owlplanner/debts.py CHANGED
@@ -1,13 +1,23 @@
1
1
  """
2
+ Debt management and calculation module.
2
3
 
3
- Owl/debts:r
4
+ This module provides functions for handling debts including mortgage calculations,
5
+ loan amortization, and debt-related financial planning.
4
6
 
5
- This file contains functions for handling debts.
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
6
8
 
7
- Copyright © 2025 - Martin-D. Lacasse
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
8
13
 
9
- Disclaimers: This code is for educational purposes only and does not constitute financial advice.
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
10
18
 
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
11
21
  """
12
22
 
13
23
  ######################################################################
@@ -15,6 +25,8 @@ import numpy as np
15
25
  import pandas as pd # noqa: F401
16
26
  from datetime import date
17
27
 
28
+ from . import utils as u
29
+
18
30
 
19
31
  def calculate_monthly_payment(principal, annual_rate, term_years):
20
32
  """
@@ -130,12 +142,16 @@ def get_debt_payments_for_year(debts_df, year):
130
142
  float
131
143
  Total annual debt payments for the year
132
144
  """
133
- if debts_df is None or debts_df.empty:
145
+ if u.is_dataframe_empty(debts_df):
134
146
  return 0.0
135
147
 
136
148
  total_payments = 0.0
137
149
 
138
150
  for _, debt in debts_df.iterrows():
151
+ # Skip if active column exists and is False (treat NaN/None as True)
152
+ if not u.is_row_active(debt):
153
+ continue
154
+
139
155
  start_year = int(debt["year"])
140
156
  term = int(debt["term"])
141
157
  end_year = start_year + term
@@ -168,12 +184,16 @@ def get_debt_balances_for_year(debts_df, year):
168
184
  float
169
185
  Total remaining debt balances at end of year
170
186
  """
171
- if debts_df is None or debts_df.empty:
187
+ if u.is_dataframe_empty(debts_df):
172
188
  return 0.0
173
189
 
174
190
  total_balance = 0.0
175
191
 
176
192
  for _, debt in debts_df.iterrows():
193
+ # Skip if active column exists and is False (treat NaN/None as True)
194
+ if not u.is_row_active(debt):
195
+ continue
196
+
177
197
  start_year = int(debt["year"])
178
198
  term = int(debt["term"])
179
199
  end_year = start_year + term
@@ -216,12 +236,16 @@ def get_debt_payments_array(debts_df, N_n, thisyear=None):
216
236
  if thisyear is None:
217
237
  thisyear = date.today().year
218
238
 
219
- if debts_df is None or debts_df.empty:
239
+ if u.is_dataframe_empty(debts_df):
220
240
  return np.zeros(N_n)
221
241
 
222
242
  payments_n = np.zeros(N_n)
223
243
 
224
244
  for _, debt in debts_df.iterrows():
245
+ # Skip if active column exists and is False (treat NaN/None as True)
246
+ if not u.is_row_active(debt):
247
+ continue
248
+
225
249
  start_year = int(debt["year"])
226
250
  term = int(debt["term"])
227
251
  end_year = start_year + term
@@ -264,13 +288,17 @@ def get_remaining_debt_balance(debts_df, N_n, thisyear=None):
264
288
  if thisyear is None:
265
289
  thisyear = date.today().year
266
290
 
267
- if debts_df is None or debts_df.empty:
291
+ if u.is_dataframe_empty(debts_df):
268
292
  return 0.0
269
293
 
270
294
  end_year = thisyear + N_n - 1
271
295
  total_balance = 0.0
272
296
 
273
297
  for _, debt in debts_df.iterrows():
298
+ # Skip if active column exists and is False (treat NaN/None as True)
299
+ if not u.is_row_active(debt):
300
+ continue
301
+
274
302
  start_year = int(debt["year"])
275
303
  term = int(debt["term"])
276
304
  loan_end_year = start_year + term
owlplanner/fixedassets.py CHANGED
@@ -1,14 +1,24 @@
1
1
  """
2
+ Fixed assets management and tax calculation module.
2
3
 
3
- Owl/fixed_assets
4
+ This module provides functions for handling fixed assets (such as real estate)
5
+ and calculating tax implications when they are sold or disposed of, including
6
+ primary residence exclusion rules.
4
7
 
5
- This file contains functions for handling fixed assets and calculating
6
- tax implications when they are sold or disposed of.
8
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
9
 
8
- Copyright &copy; 2025 - Martin-D. Lacasse
10
+ This program is free software: you can redistribute it and/or modify
11
+ it under the terms of the GNU General Public License as published by
12
+ the Free Software Foundation, either version 3 of the License, or
13
+ (at your option) any later version.
9
14
 
10
- Disclaimers: This code is for educational purposes only and does not constitute financial advice.
15
+ This program is distributed in the hope that it will be useful,
16
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ GNU General Public License for more details.
11
19
 
20
+ You should have received a copy of the GNU General Public License
21
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
12
22
  """
13
23
 
14
24
  ######################################################################
@@ -16,6 +26,8 @@ import numpy as np
16
26
  import pandas as pd # noqa: F401
17
27
  from datetime import date
18
28
 
29
+ from . import utils as u
30
+
19
31
 
20
32
  # Primary residence exclusion limits (2025 tax year)
21
33
  RESIDENCE_EXCLUSION_SINGLE = 250000
@@ -55,7 +67,9 @@ def get_fixed_assets_arrays(fixed_assets_df, N_n, thisyear=None, filing_status="
55
67
  Parameters:
56
68
  -----------
57
69
  fixed_assets_df : pd.DataFrame
58
- DataFrame with columns: name, type, basis, value, rate, yod, commission
70
+ DataFrame with columns: name, type, year, basis, value, rate, yod, commission
71
+ where 'year' is the reference year (this year or after). Basis and
72
+ value are in reference-year dollars.
59
73
  N_n : int
60
74
  Number of years in the plan (length of output arrays)
61
75
  thisyear : int, optional
@@ -76,7 +90,7 @@ def get_fixed_assets_arrays(fixed_assets_df, N_n, thisyear=None, filing_status="
76
90
  if thisyear is None:
77
91
  thisyear = date.today().year
78
92
 
79
- if fixed_assets_df is None or fixed_assets_df.empty:
93
+ if u.is_dataframe_empty(fixed_assets_df):
80
94
  return np.zeros(N_n), np.zeros(N_n), np.zeros(N_n)
81
95
 
82
96
  tax_free_n = np.zeros(N_n)
@@ -90,23 +104,54 @@ def get_fixed_assets_arrays(fixed_assets_df, N_n, thisyear=None, filing_status="
90
104
  residence_exclusion = RESIDENCE_EXCLUSION_SINGLE
91
105
 
92
106
  for _, asset in fixed_assets_df.iterrows():
107
+ # Skip if active column exists and is False (treat NaN/None as True)
108
+ if not u.is_row_active(asset):
109
+ continue
110
+
93
111
  asset_type = str(asset["type"]).lower()
94
112
  basis = float(asset["basis"])
95
- current_value = float(asset["value"])
113
+ value_at_reference = float(asset["value"]) # Value at reference year
96
114
  annual_rate = float(asset["rate"])
115
+ # Get reference year, defaulting to thisyear for backward compatibility
116
+ if "year" in asset.index and not pd.isna(asset["year"]):
117
+ reference_year = int(asset["year"])
118
+ else:
119
+ reference_year = thisyear
97
120
  yod = int(asset["yod"]) # Year of disposition
98
121
  commission_pct = float(asset["commission"]) / 100.0
99
122
 
100
- # Find the year index in the plan
101
- if yod < thisyear or yod >= thisyear + N_n:
102
- # Asset disposition is outside the plan horizon
123
+ end_year = thisyear + N_n - 1 # Last year of the plan
124
+
125
+ # Skip if asset reference year is after the plan ends
126
+ if reference_year > end_year:
127
+ continue
128
+
129
+ # Account for negative or null yod with reference to end of plan
130
+ if yod <= 0:
131
+ yod = end_year + yod + 1
132
+
133
+ # Skip if disposition is before reference year (invalid)
134
+ if yod < reference_year:
135
+ continue
136
+
137
+ # Skip if disposition is before the plan starts
138
+ if yod < thisyear:
103
139
  continue
104
140
 
141
+ # Only process assets disposed during the plan (yod <= end_year)
142
+ # IMPORTANT: Assets with yod > end_year are NOT processed here to avoid double counting.
143
+ # They are handled separately in get_fixed_assets_bequest_value().
144
+ if yod > end_year:
145
+ continue
146
+
147
+ # Disposition at beginning of yod (within plan duration)
105
148
  n = yod - thisyear
149
+ # Asset assessed at beginning of reference_year, disposed at beginning of yod
150
+ # Growth period: from start of reference_year to start of yod = (yod - reference_year) years
151
+ years_from_reference_to_disposition = yod - reference_year
106
152
 
107
153
  # Calculate future value at disposition
108
- years_to_disposition = yod - thisyear
109
- future_value = calculate_future_value(current_value, annual_rate, years_to_disposition)
154
+ future_value = calculate_future_value(value_at_reference, annual_rate, years_from_reference_to_disposition)
110
155
 
111
156
  # Calculate proceeds after commission
112
157
  commission_amount = future_value * commission_pct
@@ -170,7 +215,9 @@ def get_fixed_assets_bequest_value(fixed_assets_df, N_n, thisyear=None):
170
215
  Parameters:
171
216
  -----------
172
217
  fixed_assets_df : pd.DataFrame
173
- DataFrame with columns: name, type, basis, value, rate, yod, commission
218
+ DataFrame with columns: name, type, year, basis, value, rate, yod, commission
219
+ where 'year' is the reference year (this year or after). Basis and
220
+ value are in reference-year dollars.
174
221
  N_n : int
175
222
  Number of years in the plan
176
223
  thisyear : int, optional
@@ -186,29 +233,56 @@ def get_fixed_assets_bequest_value(fixed_assets_df, N_n, thisyear=None):
186
233
  if thisyear is None:
187
234
  thisyear = date.today().year
188
235
 
189
- if fixed_assets_df is None or fixed_assets_df.empty:
236
+ if u.is_dataframe_empty(fixed_assets_df):
190
237
  return 0.0
191
238
 
192
- years_to_end = N_n # Years from start to last year of plan, inclusively
239
+ end_year = thisyear + N_n - 1 # Last year of the plan
193
240
  total_bequest_value = 0.0
194
241
 
195
242
  for _, asset in fixed_assets_df.iterrows():
243
+ # Skip if active column exists and is False (treat NaN/None as True)
244
+ if not u.is_row_active(asset):
245
+ continue
246
+
247
+ # Get reference year, defaulting to thisyear for backward compatibility
248
+ if "year" in asset.index and not pd.isna(asset["year"]):
249
+ reference_year = int(asset["year"])
250
+ else:
251
+ reference_year = thisyear
196
252
  yod = int(asset["yod"]) # Year of disposition
197
253
 
198
- # Only consider assets with yod past the end of the plan
199
- if yod >= thisyear + N_n:
200
- current_value = float(asset["value"])
254
+ # Skip if asset reference year is after the plan ends
255
+ if reference_year > end_year:
256
+ continue
257
+
258
+ # Account for negative or null yod with reference to end of plan
259
+ if yod <= 0:
260
+ yod = end_year + yod + 1
261
+
262
+ # Skip if disposition is before reference year (invalid)
263
+ if yod < reference_year:
264
+ continue
265
+
266
+ # Only consider assets with yod past the end of the plan (not disposed during the plan)
267
+ # IMPORTANT: Assets with yod <= end_year are NOT processed here to avoid double counting.
268
+ # They are handled separately in get_fixed_assets_arrays() where they are disposed during the plan.
269
+ # These assets (yod > end_year) are assumed to be liquidated at the end of the plan and added to the bequest
270
+ if yod > end_year:
271
+ value_at_reference = float(asset["value"]) # Value at reference year
201
272
  annual_rate = float(asset["rate"])
202
273
  commission_pct = float(asset["commission"]) / 100.0
203
274
 
204
275
  # Calculate future value at the end of the plan
205
- future_value = calculate_future_value(current_value, annual_rate, years_to_end)
276
+ # Asset assessed at beginning of reference_year, liquidated at end of end_year
277
+ # Growth period: from start of reference_year to end of end_year = (end_year - reference_year + 1) years
278
+ years_from_reference_to_end = end_year - reference_year + 1
279
+ future_value = calculate_future_value(value_at_reference, annual_rate, years_from_reference_to_end)
206
280
 
207
281
  # Calculate proceeds after commission
208
282
  commission_amount = future_value * commission_pct
209
283
  proceeds = future_value - commission_amount
210
284
 
211
- # Add to total bequest value (full proceeds, no tax)
285
+ # Add to total bequest value (full proceeds, no tax - step-up in basis for heirs)
212
286
  total_bequest_value += proceeds
213
287
 
214
288
  return total_bequest_value