Skip to content

Commit

Permalink
First draft of seasonal classes. Fixes hours_per_period.
Browse files Browse the repository at this point in the history
  • Loading branch information
plocket committed Oct 11, 2023
1 parent 93297a9 commit 32e574d
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 19 deletions.
87 changes: 68 additions & 19 deletions docassemble/ALToolbox/al_income.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
"ALVehicleList",
"ALSimpleValue",
"ALSimpleValueList",
"ALItemizedValue",
"ALItemizedValueDict",
"ALItemizedValue", # Rationale for exporting this?
"ALItemizedValueDict", # Rationale for exporting this?
"ALItemizedJob",
"ALItemizedJobList",
]
Expand Down Expand Up @@ -926,7 +926,7 @@ def __str__(self) -> str:
to_stringify.append((key, "{:.2f}".format(self[key].value)))
pretty = json.dumps(to_stringify, indent=2)
return pretty


class ALItemizedJob(DAObject):
"""
Expand Down Expand Up @@ -959,14 +959,18 @@ class ALItemizedJob(DAObject):
represents how frequently the income is earned
.is_hourly {bool} (Optional) Whether the value represents a figure that the
user earns on an hourly basis, rather than for the full time period
.hours_per_period {int} (Optional) If the job is hourly, how many hours the
user works per period.
.hours_per_period {float | Decimal} (Optional) If the job is hourly, how
many hours the user works per period.
.is_seasonal {bool} (Optional) Whether the job's income changes drastically
during different times of year.
.employer {Individual} (Optional) Individual assumed to have a name and,
optionally, an address and phone.
.source {str} (Optional) The category of this item, like "public service".
Defaults to "job".
.months {ALItemizedJobMonthList} Automatically exist, but they won't be used
unless the `is_seasonal` property is set to True. Then the give monthly
values will be added into the total of the job. You can still use the job
as a regular job so that a job can be seasonal, but still accept a single
value for the whole year.
WARNING: Individual items in `.to_add` and `.to_subtract` should not be used
directly. They should only be accessed through the filtering methods of
Expand Down Expand Up @@ -999,6 +1003,11 @@ def init(self, *pargs, **kwargs):
# Money being taken out
if not hasattr(self, "to_subtract"):
self.initializeAttribute("to_subtract", ALItemizedValueDict)

# Every non-month job will have .months, though not all jobs will use them
add_months = kwargs.get('add_months', True)
if add_months:
self.initializeAttribute("months", ALItemizedJobMonthList)

def _item_value_per_times_per_year(
self, item: ALItemizedValue, times_per_year: float = 1
Expand Down Expand Up @@ -1029,26 +1038,28 @@ def _item_value_per_times_per_year(
else:
frequency_to_use = self.times_per_year

# NOTE: fixes a bug that was present < 0.8.2
try:
hours_per_period = Decimal(self.hours_per_period)
except:
log(
word(
"Your hours per period need to be just a single number, without words"
),
"danger",
)
delattr(self, "hours_per_period")
self.hours_per_period # Will cause another exception

# Both the job and the item itself need to be hourly to be
# calculated as hourly
is_hourly = hasattr(self, "is_hourly") and self.is_hourly and hasattr(item, "is_hourly") and item.is_hourly
value = item.total()

# Use the appropriate calculation
if is_hourly:
# NOTE: fixes a bug that was present < 0.8.2
# What's the bug? What's the issue #? How to test for it?
try:
hours_per_period = Decimal(self.hours_per_period)
except:
if not self.hours_per_period.isdigit():
# Shouldn't this input just be a datatype number to make sure?
log(word(
"Your hours per period need to be just a single number, without words"
), "danger",)
else:
log(word("Your hours per period may be wrong"), "danger",)
delattr(self, "hours_per_period")
self.hours_per_period # Will cause another exception

return (
value * Decimal(hours_per_period) * Decimal(frequency_to_use)
) / Decimal(times_per_year)
Expand Down Expand Up @@ -1096,6 +1107,10 @@ def gross_total(
total += self._item_value_per_times_per_year(
value, times_per_year=times_per_year
)
if hasattr(self, 'is_seasonal') and self.is_seasonal:
total += self.months.gross_total(
times_per_year=times_per_year, source=source, exclude_source=exclude_source
)
return total

def deduction_total(
Expand Down Expand Up @@ -1318,3 +1333,37 @@ def net_total(
) - self.deduction_total(
times_per_year=times_per_year, source=source, exclude_source=exclude_source
)


class ALItemizedJobMonth(ALItemizedJob):
"""
"""
def init(self, *pargs, **kwargs):
kwargs['add_months'] = kwargs.get('add_months', False)
kwargs['is_hourly'] = kwargs.get('is_hourly', False)
kwargs['times_per_year'] = kwargs.get('times_per_year', 1)
super().init(*pargs, **kwargs)

self.to_add.there_are_any = True
self.to_subtract.there_are_any = True


class ALItemizedJobMonthList(ALItemizedJobList):
"""
"""
def init(self, *pargs, **kwargs):
kwargs['source'] = kwargs.get('source', "months")
kwargs['object_type'] = kwargs.get('object_type', ALItemizedJobMonth)
kwargs['ask_number'] = kwargs.get('ask_number', True)
kwargs['target_number'] = kwargs.get('target_number', 12)

kwargs['add_months'] = kwargs.get('add_months', False)
super().init(*pargs, **kwargs)

month_names = [
"january", "february", "march", "april", "may", "june",
"july", "august", "september", "october", "november"
]
for month_name in month_names:
month = self.appendObject(source=month_name)

60 changes: 60 additions & 0 deletions docassemble/ALToolbox/data/questions/al_income_demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,66 @@ metadata:
include:
- al_income.yml
---
mandatory: True
code: |
class ALSeasonalItemizedJob(ALItemizedJobList):
"""
Attributes:
.to_add {ALItemizedValueDict} Dict of ALItemizedValues that would be added
to a job's net total, like wages and tips.
.to_subtract {ALItemizedValueDict} Dict of ALItemizedValues that would be
subtracted from a net total, like union dues or insurance premiums.
.times_per_year {float} A denominator of a year, like 12 for monthly, that
represents how frequently the income is earned
.employer {Individual} (Optional) Individual assumed to have a name and,
optionally, an address and phone.
.source {str} (Optional) The category of this item, like "public service".
"""
def init(self, *pargs, **kwargs):
super().init(*pargs, **kwargs)
self.ask_number = True
self.target_number = 12
month_names = [
"january", "february", "march", "april", "may", "june",
"july", "august", "september", "october", "november"
]
for index, month_name in enumerate(month_names):
month = self.initializeObject(index, ALJob)
month.source = month_name
month.is_hourly = False
month.times_per_year = 1
if not hasattr(self, "employer"):
if hasattr(self, "employer_type"):
self.initializeAttribute("employer", self.employer_type)
else:
self.initializeAttribute("employer", Individual)
def employer_name_address_phone(self) -> str:
"""
Returns concatenation of employer name and, if they exist, employer
address and phone number.
"""
info_list = []
has_address = (
hasattr(self.employer.address, "address") and self.employer.address.address
)
has_number = (
hasattr(self.employer, "phone_number") and self.employer.phone_number
)
# Create a list so we can take advantage of `comma_list` instead
# of doing further fiddly list manipulation
if has_address:
info_list.append(self.employer.address.on_one_line())
if has_number:
info_list.append(self.employer.phone_number)
# If either exist, add a colon and the appropriate strings
if has_address or has_number:
return (
f"{ self.employer.name.full(middle='full') }: {comma_list( info_list )}"
)
return self.employer.name.full(middle="full")
---
comment: |
translation options:
- map dict/lookup from key to lang word. See https://github.com/nonprofittechy/docassemble-HousingCodeChecklist/blob/0cbfe02b29bbec66b8a2b925b36b3c67bb300e84/docassemble/HousingCodeChecklist/data/questions/language.yml#L41
Expand Down

0 comments on commit 32e574d

Please sign in to comment.