owlplanner 2025.2.3__py3-none-any.whl → 2025.2.5__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/plan.py CHANGED
@@ -898,12 +898,9 @@ class Plan(object):
898
898
  self.myRothX_in[i, :h] = self.timeLists[iname]['Roth conv'].iloc[:h]
899
899
  self.Lambda_in[i, :h] = self.timeLists[iname]['big-ticket items'].iloc[:h]
900
900
 
901
- # In 1st year, reduce wages and contribution depending on starting date.
901
+ # In 1st year, reduce wages and contributions depending on starting date.
902
902
  self.omega_in[:, 0] *= self.yearFracLeft
903
903
  self.kappa_ijn[:, :, 0] *= self.yearFracLeft
904
- if self.yearFracLeft != 1:
905
- self.Lambda_in[:, 0] = 0
906
- self.myRothX_in[:, 0] = 0
907
904
 
908
905
  self.caseStatus = 'modified'
909
906
 
@@ -1416,11 +1413,11 @@ class Plan(object):
1416
1413
 
1417
1414
  progcall.finish()
1418
1415
  self.mylog.resetVerbose()
1419
- fig, summary = self._showResults(objective, df, N, figure)
1420
- self.mylog.print(summary.getvalue())
1416
+ fig, description = self._showResults(objective, df, N, figure)
1417
+ self.mylog.print(description.getvalue())
1421
1418
 
1422
1419
  if figure:
1423
- return fig, summary.getvalue()
1420
+ return fig, description.getvalue()
1424
1421
 
1425
1422
  return N, df
1426
1423
 
@@ -1472,11 +1469,11 @@ class Plan(object):
1472
1469
 
1473
1470
  progcall.finish()
1474
1471
  self.mylog.resetVerbose()
1475
- fig, summary = self._showResults(objective, df, N, figure)
1476
- self.mylog.print(summary.getvalue())
1472
+ fig, description = self._showResults(objective, df, N, figure)
1473
+ self.mylog.print(description.getvalue())
1477
1474
 
1478
1475
  if figure:
1479
- return fig, summary.getvalue()
1476
+ return fig, description.getvalue()
1480
1477
 
1481
1478
  return N, df
1482
1479
 
@@ -1486,9 +1483,9 @@ class Plan(object):
1486
1483
  """
1487
1484
  import seaborn as sbn
1488
1485
 
1489
- summary = io.StringIO()
1486
+ description = io.StringIO()
1490
1487
 
1491
- print('Success rate: %s on %d samples.' % (u.pc(len(df) / N), N), file=summary)
1488
+ print('Success rate: %s on %d samples.' % (u.pc(len(df) / N), N), file=description)
1492
1489
  title = '$N$ = %d, $P$ = %s' % (N, u.pc(len(df) / N))
1493
1490
  means = df.mean(axis=0, numeric_only=True)
1494
1491
  medians = df.median(axis=0, numeric_only=True)
@@ -1501,7 +1498,7 @@ class Plan(object):
1501
1498
  # or if solution led to empty accounts at the end of first spouse's life.
1502
1499
  if np.all(self.phi_j == 1) or medians.iloc[0] < 1:
1503
1500
  if medians.iloc[0] < 1:
1504
- print('Optimized solutions all have null partial bequest in year %d.' % my[0], file=summary)
1501
+ print('Optimized solutions all have null partial bequest in year %d.' % my[0], file=description)
1505
1502
  df.drop('partial', axis=1, inplace=True)
1506
1503
  means = df.mean(axis=0, numeric_only=True)
1507
1504
  medians = df.median(axis=0, numeric_only=True)
@@ -1553,16 +1550,16 @@ class Plan(object):
1553
1550
  # plt.show()
1554
1551
 
1555
1552
  for q in range(len(means)):
1556
- print('%12s: Median (%d $): %s' % (leads[q], self.year_n[0], u.d(medians.iloc[q])), file=summary)
1557
- print('%12s: Mean (%d $): %s' % (leads[q], self.year_n[0], u.d(means.iloc[q])), file=summary)
1553
+ print('%12s: Median (%d $): %s' % (leads[q], self.year_n[0], u.d(medians.iloc[q])), file=description)
1554
+ print('%12s: Mean (%d $): %s' % (leads[q], self.year_n[0], u.d(means.iloc[q])), file=description)
1558
1555
  print(
1559
1556
  '%12s: Range: %s - %s'
1560
1557
  % (leads[q], u.d(1000 * df.iloc[:, q].min()), u.d(1000 * df.iloc[:, q].max())),
1561
- file=summary)
1558
+ file=description)
1562
1559
  nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
1563
- print('%12s: N zero solns: %d' % (leads[q], nzeros), file=summary)
1560
+ print('%12s: N zero solns: %d' % (leads[q], nzeros), file=description)
1564
1561
 
1565
- return fig, summary
1562
+ return fig, description
1566
1563
 
1567
1564
  def resolve(self):
1568
1565
  """
@@ -2041,141 +2038,133 @@ class Plan(object):
2041
2038
  @_checkCaseStatus
2042
2039
  def summary(self):
2043
2040
  """
2044
- Print summary of values.
2041
+ Print summary in logs.
2045
2042
  """
2046
2043
  self.mylog.print('SUMMARY ================================================================')
2047
- lines = self.summaryList()
2048
- for line in lines:
2049
- self.mylog.print(line)
2044
+ dic = self.summaryDic()
2045
+ for key, value in dic.items():
2046
+ self.mylog.print(f"{key}: {value}")
2050
2047
  self.mylog.print('------------------------------------------------------------------------')
2051
2048
 
2052
2049
  return None
2053
2050
 
2051
+ def summaryList(self):
2052
+ """
2053
+ Return summary as a list.
2054
+ """
2055
+ mylist = []
2056
+ dic = self.summaryDic()
2057
+ for key, value in dic.items():
2058
+ mylist.append(f"{key}: {value}")
2059
+
2060
+ return mylist
2061
+
2054
2062
  def summaryString(self):
2055
2063
  """
2056
- Print summary of values in a string.
2064
+ Return summary as a string.
2057
2065
  """
2058
- string = ''
2059
- lines = self.summaryList()
2060
- for line in lines:
2061
- string += line + '\n'
2066
+ string = 'Synopsis\n'
2067
+ dic = self.summaryDic()
2068
+ for key, value in dic.items():
2069
+ string += f"{key:>70}: {value}\n"
2070
+ # string += "%60s: %s\n" % (key, value)
2062
2071
 
2063
2072
  return string
2064
2073
 
2065
- def summaryList(self):
2074
+ def summaryDic(self):
2066
2075
  """
2067
- Return string with summary of values.
2076
+ Return dictionary containing summary of values.
2068
2077
  """
2069
2078
  now = self.year_n[0]
2070
- lines = []
2071
- lines.append('Plan name: %s' % self._name)
2072
- for i in range(self.N_i):
2073
- lines.append("%12s's %02d-year life horizon: %d -> %d"
2074
- % (self.inames[i], self.horizons[i], now, now + self.horizons[i] - 1))
2075
- lines.append('Contributions file: %s' % self.timeListsFileName)
2076
- lines.append('Initial balances [taxable, tax-deferred, tax-free]:')
2077
- for i in range(self.N_i):
2078
- lines.append("%12s's accounts: %s" % (self.inames[i], [u.d(self.beta_ij[i][j]) for j in range(self.N_j)]))
2079
-
2080
- lines.append('Return rates: %s' % self.rateMethod)
2081
- if self.rateMethod in ['historical', 'historical average', 'histochastic']:
2082
- lines.append('Rates used: from %d to %d' % (self.rateFrm, self.rateTo))
2083
- elif self.rateMethod == 'stochastic':
2084
- lines.append(
2085
- 'Mean rates used (%%): %s' % (['{:.1f}'.format(100 * self.rateValues[k]) for k in range(self.N_k)])
2086
- )
2087
- lines.append(
2088
- 'Standard deviation used (%%): %s'
2089
- % (['{:.1f}'.format(100 * self.rateStdev[k]) for k in range(self.N_k)])
2090
- )
2091
- lines.append('Correlation matrix used:')
2092
- lines.append('\t\t' + str(self.rateCorr).replace('\n', '\n\t\t'))
2093
- else:
2094
- lines.append('Rates used (%%): %s' % (['{:.1f}'.format(100 * self.rateValues[k]) for k in range(self.N_k)]))
2095
- lines.append("This year's starting date: %s" % self.startDate)
2096
- lines.append('Optimized for: %s' % self.objective)
2097
- lines.append('Solver options: %s' % self.solverOptions)
2098
- lines.append('Number of decision variables: %d' % self.A.nvars)
2099
- lines.append('Number of constraints: %d' % self.A.ncons)
2100
- lines.append('Spending profile: %s' % self.spendingProfile)
2101
- if self.spendingProfile == 'smile':
2102
- lines.append('\twith increase: %d%%, dip: %d%%, delay: %dy'
2103
- % (self.smileIncrease, self.smileDip, self.smileDelay))
2104
- if self.N_i == 2:
2105
- lines.append('Surviving spouse spending needs: %s' % u.pc(self.chi, f=0))
2106
-
2107
- lines.append('Net yearly spending in year %d: %s' % (now, u.d(self.g_n[0] / self.yearFracLeft)))
2108
- lines.append('Net spending remaining in year %d: %s' % (now, u.d(self.g_n[0])))
2109
- lines.append('Net yearly spending profile basis in %d$: %s' % (now, u.d(self.g_n[0] / self.xi_n[0])))
2110
-
2111
- lines.append('Assumed heirs marginal tax rate: %s' % u.pc(self.nu, f=0))
2112
-
2113
- if self.N_i == 2 and self.n_d < self.N_n:
2114
- lines.append("Spousal surplus deposit fraction in %s's taxable account: %s"
2115
- % (self.inames[1], self.eta))
2116
- lines.append('Spousal beneficiary fractions to %s: %s' % (self.inames[self.i_s], self.phi_j.tolist()))
2117
- p_j = self.partialEstate_j * (1 - self.phi_j)
2118
- p_j[1] *= 1 - self.nu
2119
- nx = self.n_d - 1
2120
- totOthers = np.sum(p_j)
2121
- totOthersNow = totOthers / self.gamma_n[nx + 1]
2122
- q_j = self.partialEstate_j * self.phi_j
2123
- totSpousal = np.sum(q_j)
2124
- totSpousalNow = totSpousal / self.gamma_n[nx + 1]
2125
- lines.append('Spousal wealth transfer from %s to %s in year %d (nominal):'
2126
- % (self.inames[self.i_d], self.inames[self.i_s], self.year_n[nx]))
2127
- lines.append(' taxable: %s tax-def: %s tax-free: %s' % (u.d(q_j[0]), u.d(q_j[1]), u.d(q_j[2])))
2128
- lines.append('Sum of spousal bequests to %s in year %d in %d$: %s (%s nominal)'
2129
- % (self.inames[self.i_s], self.year_n[nx], now, u.d(totSpousalNow), u.d(totSpousal)))
2130
- lines.append(
2131
- 'Post-tax non-spousal bequests from %s in year %d (nominal):' % (self.inames[self.i_d], self.year_n[nx])
2132
- )
2133
- lines.append(' taxable: %s tax-def: %s tax-free: %s' % (u.d(p_j[0]), u.d(p_j[1]), u.d(p_j[2])))
2134
- lines.append(
2135
- 'Sum of post-tax non-spousal bequests from %s in year %d in %d$: %s (%s nominal)'
2136
- % (self.inames[self.i_d], self.year_n[nx], now, u.d(totOthersNow), u.d(totOthers))
2137
- )
2079
+ dic = {}
2080
+ # Results
2081
+ dic[f"Net yearly spending basis in {now}$"] = (u.d(self.g_n[0] / self.xi_n[0]))
2082
+ dic[f"Net yearly spending for year {now}"] = (u.d(self.g_n[0] / self.yearFracLeft))
2083
+ dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
2138
2084
 
2139
2085
  totIncome = np.sum(self.g_n, axis=0)
2140
2086
  totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
2141
- lines.append('Total net spending in %d$: %s (%s nominal)' % (now, u.d(totIncomeNow), u.d(totIncome)))
2087
+ dic[f"Total net spending in {now}$"] = (
2088
+ "%s (%s nominal)" % (u.d(totIncomeNow), u.d(totIncome))
2089
+ )
2142
2090
 
2143
2091
  totRoth = np.sum(self.x_in, axis=(0, 1))
2144
2092
  totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
2145
- lines.append('Total Roth conversions in %d$: %s (%s nominal)' % (now, u.d(totRothNow), u.d(totRoth)))
2093
+ dic[f"Total Roth conversions in {now}$"] = (
2094
+ "%s (%s nominal)" % (u.d(totRothNow), u.d(totRoth))
2095
+ )
2146
2096
 
2147
2097
  taxPaid = np.sum(self.T_n, axis=0)
2148
2098
  taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
2149
- lines.append('Total income tax paid on ordinary income in %d$: %s (%s nominal)'
2150
- % (now, u.d(taxPaidNow), u.d(taxPaid)))
2099
+ dic[f"Total income tax paid on ordinary income in {now}$"] = (
2100
+ "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2101
+ )
2151
2102
 
2152
2103
  taxPaid = np.sum(self.U_n, axis=0)
2153
2104
  taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
2154
- lines.append('Total tax paid on gains and dividends in %d$: %s (%s nominal)'
2155
- % (now, u.d(taxPaidNow), u.d(taxPaid)))
2105
+ dic[f"Total tax paid on gains and dividends in {now}$"] = (
2106
+ "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2107
+ )
2156
2108
 
2157
2109
  taxPaid = np.sum(self.M_n, axis=0)
2158
2110
  taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
2159
- lines.append('Total Medicare premiums paid in %d$: %s (%s nominal)' % (now, u.d(taxPaidNow), u.d(taxPaid)))
2111
+ dic[f"Total Medicare premiums paid in {now}$"] = (
2112
+ "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2113
+ )
2114
+
2115
+ if self.N_i == 2 and self.n_d < self.N_n:
2116
+ p_j = self.partialEstate_j * (1 - self.phi_j)
2117
+ p_j[1] *= 1 - self.nu
2118
+ nx = self.n_d - 1
2119
+ totOthers = np.sum(p_j)
2120
+ totOthersNow = totOthers / self.gamma_n[nx + 1]
2121
+ q_j = self.partialEstate_j * self.phi_j
2122
+ totSpousal = np.sum(q_j)
2123
+ totSpousalNow = totSpousal / self.gamma_n[nx + 1]
2124
+ dic["Spousal wealth transfer from %s to %s in year %d (nominal)" %
2125
+ (self.inames[self.i_d], self.inames[self.i_s], self.year_n[nx])] = (
2126
+ "taxable: %s tax-def: %s tax-free: %s" % (u.d(q_j[0]), u.d(q_j[1]), u.d(q_j[2]))
2127
+ )
2128
+
2129
+ dic["Sum of spousal bequests to %s in year %d in %d$" %
2130
+ (self.inames[self.i_s], self.year_n[nx], now)] = (
2131
+ "%s (%s nominal)" % (u.d(totSpousalNow), u.d(totSpousal))
2132
+ )
2133
+ dic["Post-tax non-spousal bequests from %s in year %d (nominal)" %
2134
+ (self.inames[self.i_d], self.year_n[nx])] = (
2135
+ "taxable: %s tax-def: %s tax-free: %s" % (u.d(p_j[0]), u.d(p_j[1]), u.d(p_j[2]))
2136
+ )
2137
+ dic["Sum of post-tax non-spousal bequests from %s in year %d in %d$" %
2138
+ (self.inames[self.i_d], self.year_n[nx], now)] = (
2139
+ "%s (%s nominal)" % (u.d(totOthersNow), u.d(totOthers))
2140
+ )
2160
2141
 
2161
2142
  estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
2162
2143
  estate[1] *= 1 - self.nu
2163
- lines.append('Post-tax account values at the end of final plan year %d: (nominal)' % self.year_n[-1])
2164
- lines.append(' taxable: %s tax-def: %s tax-free: %s' % (u.d(estate[0]), u.d(estate[1]), u.d(estate[2])))
2144
+ dic["Post-tax account values at the end of final plan year %d (nominal)" % self.year_n[-1]] = (
2145
+ "taxable: %s tax-def: %s tax-free: %s" % (u.d(estate[0]), u.d(estate[1]), u.d(estate[2]))
2146
+ )
2165
2147
 
2166
2148
  totEstate = np.sum(estate)
2167
2149
  totEstateNow = totEstate / self.gamma_n[-1]
2168
- lines.append(
2169
- 'Total estate value at the end of final plan year %d in %d$: %s (%s nominal)'
2170
- % (self.year_n[-1], now, u.d(totEstateNow), u.d(totEstate))
2150
+ dic["Total estate value at the end of final plan year %d in %d$" % (self.year_n[-1], now)] = (
2151
+ "%s (%s nominal)" % (u.d(totEstateNow), u.d(totEstate))
2171
2152
  )
2172
- lines.append(
2173
- "Inflation factor from this year's start date to the end of plan final year: %.2f" % self.gamma_n[-1]
2153
+ dic["Plan starting date"] = str(self.startDate)
2154
+ dic["Cumulative inflation factor from start date to end of plan"] = (
2155
+ "%.2f" % (self.gamma_n[-1])
2174
2156
  )
2157
+ for i in range(self.N_i):
2158
+ dic["%12s's %02d-year life horizon" % (self.inames[i], self.horizons[i])] = (
2159
+ "%d -> %d" % (now, now + self.horizons[i] - 1)
2160
+ )
2175
2161
 
2176
- lines.append('Case executed on: %s' % self._timestamp)
2162
+ dic["Plan name"] = self._name
2163
+ dic["Number of decision variables"] = str(self.A.nvars)
2164
+ dic["Number of constraints"] = str(self.A.ncons)
2165
+ dic["Case executed on"] = str(self._timestamp)
2177
2166
 
2178
- return lines
2167
+ return dic
2179
2168
 
2180
2169
  def showRatesCorrelations(self, tag='', shareRange=False, figure=False):
2181
2170
  """
owlplanner/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.02.03"
1
+ __version__ = "2025.02.05"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.2.3
3
+ Version: 2025.2.5
4
4
  Summary: Owl: Retirement planner with great wisdom
5
5
  Project-URL: HomePage, https://github.com/mdlacasse/owl
6
6
  Project-URL: Repository, https://github.com/mdlacasse/owl
@@ -731,7 +731,8 @@ This is exactly where this tool fits it. Given your savings capabilities and spe
731
731
  it can generate different future realizations of
732
732
  your strategy under different market assumptions, helping to better understand your financial situation.
733
733
 
734
- Disclaimers: I am not a financial planner. You make your own decisions. This program comes with no guarantee. Use at your own risk.
734
+ Disclaimers: I am not a financial planner. You make your own decisions.
735
+ This program comes with no guarantee. Use at your own risk.
735
736
 
736
737
  More disclaimers: While some output of the code has been verified with other approaches,
737
738
  this code is still under development and I cannot guarantee the accuracy of the results.
@@ -975,47 +976,31 @@ The output of the last command reports that if future rates are exactly like tho
975
976
  starting from 1969 and the following years, Jack and Jill could afford an annual spending of
976
977
  \\$97k starting this year
977
978
  (with a basis of \\$88.8k - the basis multiplies the profile which can vary over the course of the plan).
978
- The summary also contains many more details:
979
+ The summary also contains some details:
979
980
  ```
980
981
  SUMMARY ================================================================
982
+ Net yearly spending basis in 2025$: $91,812
983
+ Net yearly spending for year 2025: $100,448
984
+ Net spending remaining in year 2025: $100,448
985
+ Total net spending in 2025$: $2,809,453 ($7,757,092 nominal)
986
+ Total Roth conversions in 2025$: $320,639 ($456,454 nominal)
987
+ Total income tax paid on ordinary income in 2025$: $247,788 ($469,522 nominal)
988
+ Total tax paid on gains and dividends in 2025$: $3,313 ($3,768 nominal)
989
+ Total Medicare premiums paid in 2025$: $117,660 ($343,388 nominal)
990
+ Spousal wealth transfer from Jack to Jill in year 2051 (nominal): taxable: $0 tax-def: $57,224 tax-free: $2,102,173
991
+ Sum of spousal bequests to Jill in year 2051 in 2025$: $499,341 ($2,159,397 nominal)
992
+ Post-tax non-spousal bequests from Jack in year 2051 (nominal): taxable: $0 tax-def: $0 tax-free: $0
993
+ Sum of post-tax non-spousal bequests from Jack in year 2051 in 2025$: $0 ($0 nominal)
994
+ Post-tax account values at the end of final plan year 2057 (nominal): taxable: $0 tax-def: $0 tax-free: $2,488,808
995
+ Total estate value at the end of final plan year 2057 in 2025$: $500,000 ($2,488,808 nominal)
996
+ Plan starting date: 01-01
997
+ Cumulative inflation factor from start date to end of plan: 4.98
998
+ Jack's 27-year life horizon: 2025 -> 2051
999
+ Jill's 33-year life horizon: 2025 -> 2057
981
1000
  Plan name: jack & jill - tutorial
982
- Jack's life horizon: 2024 -> 2051
983
- Jill's life horizon: 2024 -> 2057
984
- Contributions file: examples/jack+jill.xlsx
985
- Initial balances [taxable, tax-deferred, tax-free]:
986
- Jack's accounts: ['$90,500', '$600,500', '$70,000']
987
- Jill's accounts: ['$60,200', '$150,000', '$40,000']
988
- Return rates: historical
989
- Rates used: from 1969 to 2002
990
- This year's starting date: 01-01
991
- Optimized for: maxSpending
992
- Solver options: {'maxRothConversion': 100, 'bequest': 500, 'noRothConversions': 'Jill'}
993
- Number of decision variables: 1026
994
- Number of constraints: 894
995
- Spending profile: smile
996
- Surviving spouse spending needs: 60%
997
- Net yearly spending in year 2024: $97,098
998
- Net spending remaining in year 2024: $97,098
999
- Net yearly spending profile basis in 2024$: $88,763
1000
- Assumed heirs tax rate: 30%
1001
- Spousal surplus deposit fraction: 0.5
1002
- Spousal beneficiary fractions to Jill: [1, 1, 1]
1003
- Spousal wealth transfer from Jack to Jill in year 2051 (nominal):
1004
- taxable: $0 tax-def: $63,134 tax-free: $2,583,303
1005
- Sum of spousal bequests to Jill in year 2051 in 2024$: $592,103 ($2,646,437 nominal)
1006
- Post-tax non-spousal bequests from Jack in year 2051 (nominal):
1007
- taxable: $0 tax-def: $0 tax-free: $0
1008
- Sum of post-tax non-spousal bequests from Jack in year 2051 in 2024$: $0 ($0 nominal)
1009
- Total net spending in 2024$: $2,804,910 ($7,916,623 nominal)
1010
- Total Roth conversions in 2024$: $311,760 ($443,005 nominal)
1011
- Total ordinary income tax paid in 2024$: $236,710 ($457,922 nominal)
1012
- Total dividend tax paid in 2024$: $3,437 ($3,902 nominal)
1013
- Total Medicare premiums paid in 2024$: $117,817 ($346,404 nominal)
1014
- Post-tax account values at the end of final plan year 2057: (nominal)
1015
- taxable: $0 tax-def: $0 tax-free: $2,553,871
1016
- Total estate value at the end of final plan year 2057 in 2024$: $500,000 ($2,553,871 nominal)
1017
- Inflation factor from this year's start date to the end of plan final year: 5.11
1018
- Case executed on: 2024-12-09 at 22:11:57
1001
+ Number of decision variables: 996
1002
+ Number of constraints: 867
1003
+ Case executed on: 2025-02-04 at 22:55:03
1019
1004
  ------------------------------------------------------------------------
1020
1005
  ```
1021
1006
  And an Excel workbook can be saved with all the detailed amounts over the years by using the following command:
@@ -2,16 +2,16 @@ owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
2
2
  owlplanner/abcapi.py,sha256=eemIsdbtzdWCIj5VuuswgphxXMcxJ_GZfUlDi6lttFM,6658
3
3
  owlplanner/config.py,sha256=ouADb6YES5Zgv0UwnEK9Axwvs8drp-ahboQjI4WTrr0,12069
4
4
  owlplanner/logging.py,sha256=pXg_mMgBll-kklqaDRLDNVUFo-5DAa-yqTKtiVrhNWw,2530
5
- owlplanner/plan.py,sha256=GdN122pLHznjfp4mi7yFqCnd2_MdfZsQ5LcFYqi3wdI,115647
5
+ owlplanner/plan.py,sha256=LJwNf6UngC2WepPyvO6zQzx9V0RbsQd2H9LAg5V8Fto,113815
6
6
  owlplanner/progress.py,sha256=YZjL5_m4MMgKPlWlhhKacPLt54tVhVGF1eXxxZapMYs,386
7
7
  owlplanner/rates.py,sha256=aKOmau8i3uqxZGi7HQJpzooT3X-yAZhga5MZJ56pBzk,15627
8
8
  owlplanner/tax2025.py,sha256=b2RgM6TBQa8ggo6ODyh0p_J7j79UUm8z5NiENqa1l_k,7016
9
9
  owlplanner/timelists.py,sha256=WwymsYAGWcrEzMtc-wrLbn1EVA2fhqXGN4NHLJsH3Fs,4110
10
10
  owlplanner/utils.py,sha256=adIwqGVQFfvekke0JCxYJD3PKHbptVCj3NrQT2TQIB4,2351
11
- owlplanner/version.py,sha256=TnYrEa0bKIv3AC32iy7hg0cAWMxUqpWDseaUPdO3-3c,28
11
+ owlplanner/version.py,sha256=3SB4x2fk8FWUTRIEqgNzXA2CeRNyNlNPfgW0-yoyV_A,28
12
12
  owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
- owlplanner-2025.2.3.dist-info/METADATA,sha256=KUCTbvMBOtv1ZwB_k7Z7Bs046T-R0dsntWi_SYsp-0Q,64514
15
- owlplanner-2025.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- owlplanner-2025.2.3.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
- owlplanner-2025.2.3.dist-info/RECORD,,
14
+ owlplanner-2025.2.5.dist-info/METADATA,sha256=RpUVgZp48DCU53VP6HjcQvojsnaZPsqSaav2jJ0OglA,63946
15
+ owlplanner-2025.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ owlplanner-2025.2.5.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
+ owlplanner-2025.2.5.dist-info/RECORD,,