owlplanner 2025.2.21__tar.gz → 2025.2.23__tar.gz

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.
Files changed (101) hide show
  1. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/PKG-INFO +1 -1
  2. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/case_jack+jill.toml +1 -0
  3. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/case_joe.toml +1 -0
  4. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/case_john+sally.toml +1 -0
  5. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/case_jon+jane.toml +1 -0
  6. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/case_kim+sam-bequest.toml +1 -0
  7. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/case_kim+sam-spending.toml +1 -0
  8. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/pyproject.toml +1 -1
  9. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/config.py +49 -47
  10. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/plan.py +9 -0
  11. owlplanner-2025.2.23/src/owlplanner/version.py +1 -0
  12. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Create_Case.py +6 -2
  13. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/owlbridge.py +8 -1
  14. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/requirements.txt +1 -1
  15. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/sskeys.py +15 -1
  16. owlplanner-2025.2.21/src/owlplanner/version.py +0 -1
  17. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/.devcontainer/devcontainer.json +0 -0
  18. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/.flake8 +0 -0
  19. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/.gitattributes +0 -0
  20. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/.github/workflows/github-actions-runtests.yml +0 -0
  21. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/.gitignore +0 -0
  22. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/INSTALL.md +0 -0
  23. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/LICENSE +0 -0
  24. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/README.md +0 -0
  25. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/USER_GUIDE.md +0 -0
  26. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docker/Dockerfile +0 -0
  27. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docker/README.md +0 -0
  28. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docker/docker-compose.yml +0 -0
  29. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docker/fastentrypoint.sh +0 -0
  30. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/AD-taxDef.png +0 -0
  31. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/AD-taxFree.png +0 -0
  32. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/AD-taxable.png +0 -0
  33. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/Hist_Bequest.png +0 -0
  34. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/Hist_Spending.png +0 -0
  35. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/MC-tutorial2a.png +0 -0
  36. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/MC-tutorial2b.png +0 -0
  37. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/OwlUI.png +0 -0
  38. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/allocations.png +0 -0
  39. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/owl.png +0 -0
  40. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/profile.png +0 -0
  41. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/ratesCorrelations.png +0 -0
  42. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/ratesPlot.png +0 -0
  43. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/savingsPlot.png +0 -0
  44. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/sourcesPlot.png +0 -0
  45. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/spendingPlot.png +0 -0
  46. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/taxIncomePlot.png +0 -0
  47. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/images/taxesPlot.png +0 -0
  48. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/owl.pdf +0 -0
  49. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/docs/owl.tex +0 -0
  50. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/jack+jill.xlsx +0 -0
  51. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/joe.xlsx +0 -0
  52. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/john+sally.xlsx +0 -0
  53. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/jon+jane.xlsx +0 -0
  54. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/examples/template.xlsx +0 -0
  55. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/notebooks/john+sally.ipynb +0 -0
  56. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/notebooks/kim+sam.ipynb +0 -0
  57. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/notebooks/template.ipynb +0 -0
  58. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/notebooks/tutorial_1.ipynb +0 -0
  59. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/notebooks/tutorial_2.ipynb +0 -0
  60. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/notebooks/tutorial_3.ipynb +0 -0
  61. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/owlplanner.cmd +0 -0
  62. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/owlplanner.sh +0 -0
  63. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/requirements.txt +0 -0
  64. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/__init__.py +0 -0
  65. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/abcapi.py +0 -0
  66. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/data/__init__.py +0 -0
  67. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/data/rates.csv +0 -0
  68. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/logging.py +0 -0
  69. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/progress.py +0 -0
  70. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/rates.py +0 -0
  71. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/tax2025.py +0 -0
  72. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/timelists.py +0 -0
  73. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/src/owlplanner/utils.py +0 -0
  74. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/tests/test_logger.py +0 -0
  75. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/tests/test_regressions.py +0 -0
  76. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/tests/test_repro.py +0 -0
  77. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/tests/test_toml_cases.py +0 -0
  78. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/tests/test_units.py +0 -0
  79. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ttt.py +0 -0
  80. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/About_Owl.py +0 -0
  81. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Asset_Allocation.py +0 -0
  82. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Assets.py +0 -0
  83. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Documentation.py +0 -0
  84. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Fixed_Income.py +0 -0
  85. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Graphs.py +0 -0
  86. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Historical_Range.py +0 -0
  87. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Logs.py +0 -0
  88. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Monte_Carlo.py +0 -0
  89. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Optimization_Parameters.py +0 -0
  90. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Output_Files.py +0 -0
  91. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Quick_Start.py +0 -0
  92. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/README.md +0 -0
  93. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Rates_Selection.py +0 -0
  94. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Settings.py +0 -0
  95. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Wages_And_Contributions.py +0 -0
  96. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/Worksheets.py +0 -0
  97. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/main+fonts.py +0 -0
  98. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/main.py +0 -0
  99. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/plots.py +0 -0
  100. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/progress.py +0 -0
  101. {owlplanner-2025.2.21 → owlplanner-2025.2.23}/ui/style.css +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.2.21
3
+ Version: 2025.2.23
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
@@ -1,4 +1,5 @@
1
1
  "Plan Name" = "jack+jill"
2
+ Description = "This example case is for Jack and Jill, a married couple."
2
3
 
3
4
  ["Basic Info"]
4
5
  Status = "married"
@@ -1,4 +1,5 @@
1
1
  "Plan Name" = "joe"
2
+ Description = "This example case is for Joe, a single individual."
2
3
 
3
4
  ["Basic Info"]
4
5
  Status = "single"
@@ -1,4 +1,5 @@
1
1
  "Plan Name" = "john+sally"
2
+ Description = "This example reproduces the case of John and Sally, discussed by Eric Sajdak."
2
3
 
3
4
  ["Basic Info"]
4
5
  Status = "married"
@@ -1,4 +1,5 @@
1
1
  "Plan Name" = "Jon+Jane"
2
+ Description = "The case of Jon and Jane was the base case discussed by i-orp."
2
3
 
3
4
  ["Basic Info"]
4
5
  Status = "married"
@@ -1,4 +1,5 @@
1
1
  "Plan Name" = "kim+sam-bequest"
2
+ Description = "Kim and Sam is an example case for optimizing bequest and Roth conversions."
2
3
 
3
4
  ["Basic Info"]
4
5
  Status = "married"
@@ -1,4 +1,5 @@
1
1
  "Plan Name" = "kim+sam-spending"
2
+ Description = "Kim and Sam is an example case for optimizing spending and Roth conversions."
2
3
 
3
4
  ["Basic Info"]
4
5
  Status = "married"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.02.21"
7
+ version = "2025.02.23"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -19,7 +19,7 @@ from owlplanner import logging
19
19
  from owlplanner.rates import FROM, TO
20
20
 
21
21
 
22
- def saveConfig(plan, file, mylog):
22
+ def saveConfig(myplan, file, mylog):
23
23
  """
24
24
  Save case parameters and return a dictionary containing all parameters.
25
25
  """
@@ -27,86 +27,87 @@ def saveConfig(plan, file, mylog):
27
27
  accountTypes = ["taxable", "tax-deferred", "tax-free"]
28
28
 
29
29
  diconf = {}
30
- diconf["Plan Name"] = plan._name
30
+ diconf["Plan Name"] = myplan._name
31
+ diconf["Description"] = myplan._description
31
32
 
32
33
  # Basic Info.
33
34
  diconf["Basic Info"] = {
34
- "Status": ["unknown", "single", "married"][plan.N_i],
35
- "Names": plan.inames,
36
- "Birth year": plan.yobs.tolist(),
37
- "Life expectancy": plan.expectancy.tolist(),
38
- "Start date": plan.startDate,
35
+ "Status": ["unknown", "single", "married"][myplan.N_i],
36
+ "Names": myplan.inames,
37
+ "Birth year": myplan.yobs.tolist(),
38
+ "Life expectancy": myplan.expectancy.tolist(),
39
+ "Start date": myplan.startDate,
39
40
  }
40
41
 
41
42
  # Assets.
42
43
  diconf["Assets"] = {}
43
- for j in range(plan.N_j):
44
- amounts = plan.beta_ij[:, j] / 1000
44
+ for j in range(myplan.N_j):
45
+ amounts = myplan.beta_ij[:, j] / 1000
45
46
  diconf["Assets"][f"{accountTypes[j]} savings balances"] = amounts.tolist()
46
- if plan.N_i == 2:
47
- diconf["Assets"]["Beneficiary fractions"] = plan.phi_j.tolist()
48
- diconf["Assets"]["Spousal surplus deposit fraction"] = plan.eta
47
+ if myplan.N_i == 2:
48
+ diconf["Assets"]["Beneficiary fractions"] = myplan.phi_j.tolist()
49
+ diconf["Assets"]["Spousal surplus deposit fraction"] = myplan.eta
49
50
 
50
51
  # Wages and Contributions.
51
- diconf["Wages and Contributions"] = {"Contributions file name": plan.timeListsFileName}
52
+ diconf["Wages and Contributions"] = {"Contributions file name": myplan.timeListsFileName}
52
53
 
53
54
  # Fixed Income.
54
55
  diconf["Fixed Income"] = {
55
- "Pension amounts": (plan.pensionAmounts / 1000).tolist(),
56
- "Pension ages": plan.pensionAges.tolist(),
57
- "Pension indexed": plan.pensionIndexed,
58
- "Social security amounts": (plan.ssecAmounts / 1000).tolist(),
59
- "Social security ages": plan.ssecAges.tolist(),
56
+ "Pension amounts": (myplan.pensionAmounts / 1000).tolist(),
57
+ "Pension ages": myplan.pensionAges.tolist(),
58
+ "Pension indexed": myplan.pensionIndexed,
59
+ "Social security amounts": (myplan.ssecAmounts / 1000).tolist(),
60
+ "Social security ages": myplan.ssecAges.tolist(),
60
61
  }
61
62
 
62
63
  # Rates Selection.
63
64
  diconf["Rates Selection"] = {
64
- "Heirs rate on tax-deferred estate": float(100 * plan.nu),
65
- "Long-term capital gain tax rate": float(100 * plan.psi),
66
- "Dividend tax rate": float(100 * plan.mu),
67
- "TCJA expiration year": plan.yTCJA,
68
- "Method": plan.rateMethod,
65
+ "Heirs rate on tax-deferred estate": float(100 * myplan.nu),
66
+ "Long-term capital gain tax rate": float(100 * myplan.psi),
67
+ "Dividend tax rate": float(100 * myplan.mu),
68
+ "TCJA expiration year": myplan.yTCJA,
69
+ "Method": myplan.rateMethod,
69
70
  }
70
- if plan.rateMethod in ["user", "stochastic"]:
71
- diconf["Rates Selection"]["Values"] = (100 * plan.rateValues).tolist()
72
- if plan.rateMethod in ["stochastic"]:
73
- diconf["Rates Selection"]["Standard deviations"] = (100 * plan.rateStdev).tolist()
74
- diconf["Rates Selection"]["Correlations"] = plan.rateCorr.tolist()
75
- if plan.rateMethod in ["historical average", "historical", "histochastic"]:
76
- diconf["Rates Selection"]["From"] = int(plan.rateFrm)
77
- diconf["Rates Selection"]["To"] = int(plan.rateTo)
71
+ if myplan.rateMethod in ["user", "stochastic"]:
72
+ diconf["Rates Selection"]["Values"] = (100 * myplan.rateValues).tolist()
73
+ if myplan.rateMethod in ["stochastic"]:
74
+ diconf["Rates Selection"]["Standard deviations"] = (100 * myplan.rateStdev).tolist()
75
+ diconf["Rates Selection"]["Correlations"] = myplan.rateCorr.tolist()
76
+ if myplan.rateMethod in ["historical average", "historical", "histochastic"]:
77
+ diconf["Rates Selection"]["From"] = int(myplan.rateFrm)
78
+ diconf["Rates Selection"]["To"] = int(myplan.rateTo)
78
79
  else:
79
80
  diconf["Rates Selection"]["From"] = int(FROM)
80
81
  diconf["Rates Selection"]["To"] = int(TO)
81
82
 
82
83
  # Asset Allocation.
83
84
  diconf["Asset Allocation"] = {
84
- "Interpolation method": plan.interpMethod,
85
- "Interpolation center": float(plan.interpCenter),
86
- "Interpolation width": float(plan.interpWidth),
87
- "Type": plan.ARCoord,
85
+ "Interpolation method": myplan.interpMethod,
86
+ "Interpolation center": float(myplan.interpCenter),
87
+ "Interpolation width": float(myplan.interpWidth),
88
+ "Type": myplan.ARCoord,
88
89
  }
89
- if plan.ARCoord == "account":
90
+ if myplan.ARCoord == "account":
90
91
  for accType in accountTypes:
91
- diconf["Asset Allocation"][accType] = plan.boundsAR[accType]
92
+ diconf["Asset Allocation"][accType] = myplan.boundsAR[accType]
92
93
  else:
93
- diconf["Asset Allocation"]["generic"] = plan.boundsAR["generic"]
94
+ diconf["Asset Allocation"]["generic"] = myplan.boundsAR["generic"]
94
95
 
95
96
  # Optimization Parameters.
96
97
  diconf["Optimization Parameters"] = {
97
- "Spending profile": plan.spendingProfile,
98
- "Surviving spouse spending percent": int(100 * plan.chi),
98
+ "Spending profile": myplan.spendingProfile,
99
+ "Surviving spouse spending percent": int(100 * myplan.chi),
99
100
  }
100
- if plan.spendingProfile == "smile":
101
- diconf["Optimization Parameters"]["Smile dip"] = int(plan.smileDip)
102
- diconf["Optimization Parameters"]["Smile increase"] = int(plan.smileIncrease)
103
- diconf["Optimization Parameters"]["Smile delay"] = int(plan.smileDelay)
101
+ if myplan.spendingProfile == "smile":
102
+ diconf["Optimization Parameters"]["Smile dip"] = int(myplan.smileDip)
103
+ diconf["Optimization Parameters"]["Smile increase"] = int(myplan.smileIncrease)
104
+ diconf["Optimization Parameters"]["Smile delay"] = int(myplan.smileDelay)
104
105
 
105
- diconf["Optimization Parameters"]["Objective"] = plan.objective
106
- diconf["Solver Options"] = plan.solverOptions
106
+ diconf["Optimization Parameters"]["Objective"] = myplan.objective
107
+ diconf["Solver Options"] = myplan.solverOptions
107
108
 
108
109
  # Results.
109
- diconf["Results"] = {"Default plots": plan.defaultPlots}
110
+ diconf["Results"] = {"Default plots": myplan.defaultPlots}
110
111
 
111
112
  if isinstance(file, str):
112
113
  filename = file
@@ -185,6 +186,7 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
185
186
  s = ["", "s"][icount - 1]
186
187
  mylog.vprint(f"Plan for {icount} individual{s}: {inames}.")
187
188
  p = plan.Plan(inames, yobs, expectancy, name, startDate=startDate, verbose=True, logstreams=logstreams)
189
+ p._description = diconf.get("Description", "")
188
190
 
189
191
  # Assets.
190
192
  balances = {}
@@ -239,6 +239,7 @@ class Plan(object):
239
239
  self.interpCenter = 15
240
240
  self.interpWidth = 5
241
241
 
242
+ self._description = ''
242
243
  self.defaultPlots = "nominal"
243
244
  self.defaultSolver = "HiGHS"
244
245
 
@@ -406,6 +407,12 @@ class Plan(object):
406
407
  self.mylog.vprint(f"Renaming plan {self._name} -> {newname}.")
407
408
  self._name = newname
408
409
 
410
+ def setDescription(self, description):
411
+ """
412
+ Set a text description of the plan.
413
+ """
414
+ self._description = description
415
+
409
416
  def setSpousalDepositFraction(self, eta):
410
417
  """
411
418
  Set spousal deposit and withdrawal fraction. Default 0.5.
@@ -2069,6 +2076,8 @@ class Plan(object):
2069
2076
  now = self.year_n[0]
2070
2077
  dic = {}
2071
2078
  # Results
2079
+ dic["Plan name"] = self._name
2080
+ dic["Brief description"] = self._description
2072
2081
  dic[f"Net yearly spending basis in {now}$"] = u.d(self.g_n[0] / self.xi_n[0])
2073
2082
  dic[f"Net yearly spending for year {now}"] = u.d(self.g_n[0] / self.yearFracLeft)
2074
2083
  dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
@@ -0,0 +1 @@
1
+ __version__ = "2025.02.23"
@@ -17,7 +17,7 @@ if ret == kz.newCase:
17
17
  key="_newcase",
18
18
  on_change=kz.createNewCase,
19
19
  args=["newcase"],
20
- placeholder="Enter a name...",
20
+ placeholder="Enter a case name...",
21
21
  )
22
22
  elif ret == kz.loadCaseFile:
23
23
  # "<a href="Documentation" target="_self">Documentation</a>", unsafe_allow_html=True)
@@ -40,7 +40,6 @@ else:
40
40
  on_change=kz.renameCase,
41
41
  args=["caseNewName"],
42
42
  key="_caseNewName",
43
- placeholder="Enter a name",
44
43
  help=helpmsg,
45
44
  )
46
45
 
@@ -61,6 +60,11 @@ else:
61
60
  horizontal=True,
62
61
  )
63
62
 
63
+ kz.initKey("description", "")
64
+ helpmsg = "Provide a short distinguishing description for the case."
65
+ description = kz.getLongText("Brief description", "description", help=helpmsg,
66
+ placeholder="Enter a brief description...")
67
+
64
68
  col1, col2 = st.columns(2, gap="large", vertical_alignment="top")
65
69
  with col1:
66
70
  kz.initKey("iname0", "")
@@ -20,6 +20,7 @@ def hasMOSEK():
20
20
  def createPlan():
21
21
  name = kz.currentCaseName()
22
22
  inames = [kz.getKey("iname0")]
23
+ description = kz.getKey("description")
23
24
  yobs = [kz.getKey("yob0")]
24
25
  life = [kz.getKey("life0")]
25
26
  startDate = kz.getKey("startDate")
@@ -37,6 +38,7 @@ def createPlan():
37
38
  st.error(f"Failed creation of plan '{name}': {e}")
38
39
  return
39
40
 
41
+ plan.setDescription(description)
40
42
  val = kz.getKey("plots")
41
43
  if val is not None:
42
44
  plan.setDefaultPlots(val)
@@ -98,6 +100,7 @@ def prepareRun(plan):
98
100
  st.error(f"Failed setting beneficiary fractions: {e}")
99
101
  return
100
102
 
103
+ plan.setDescription(kz.getKey("description"))
101
104
  plan.setHeirsTaxRate(kz.getKey("heirsTx"))
102
105
  plan.setLongTermCapitalTaxRate(kz.getKey("gainTx"))
103
106
  plan.setDividendRate(kz.getKey("divRate"))
@@ -422,8 +425,11 @@ def plotSingleResults(plan):
422
425
  st.write("##### Assets Distribution")
423
426
  morecols = st.columns(3, gap="small")
424
427
  for fig in figs:
425
- if fig is not None:
428
+ if fig:
426
429
  morecols[c].pyplot(fig)
430
+ else:
431
+ morecols[c].write("#\n<div style='text-align: center'> This plot is empty </div>",
432
+ unsafe_allow_html=True)
427
433
  c = (c + 1) % 3
428
434
 
429
435
 
@@ -558,6 +564,7 @@ def genDic(plan):
558
564
  dic = {}
559
565
  dic["plan"] = plan
560
566
  dic["name"] = plan._name
567
+ dic["description"] = plan._description
561
568
  dic["summary"] = ""
562
569
  dic["casetoml"] = ""
563
570
  dic["caseStatus"] = "new"
@@ -7,4 +7,4 @@ scipy
7
7
  streamlit
8
8
  toml
9
9
  # --extra-index-url https://test.pypi.org/simple
10
- owlplanner >= 2025.02.21
10
+ owlplanner >= 2025.02.23
@@ -438,7 +438,7 @@ def getNum(
438
438
  )
439
439
 
440
440
 
441
- def getText(text, nkey, disabled=False, callback=setpull, placeholder=None):
441
+ def getText(text, nkey, disabled=False, callback=setpull, placeholder=None, help=None):
442
442
  return st.text_input(
443
443
  text,
444
444
  value=getKey(nkey),
@@ -447,6 +447,20 @@ def getText(text, nkey, disabled=False, callback=setpull, placeholder=None):
447
447
  args=[nkey],
448
448
  key="_" + nkey,
449
449
  placeholder=placeholder,
450
+ help=help,
451
+ )
452
+
453
+
454
+ def getLongText(text, nkey, disabled=False, callback=setpull, placeholder=None, help=None):
455
+ return st.text_area(
456
+ text,
457
+ value=getKey(nkey),
458
+ disabled=disabled,
459
+ on_change=callback,
460
+ args=[nkey],
461
+ key="_" + nkey,
462
+ placeholder=placeholder,
463
+ help=help,
450
464
  )
451
465
 
452
466
 
@@ -1 +0,0 @@
1
- __version__ = "2025.02.21"
File without changes
File without changes
File without changes
File without changes