commit
fd6b8df18a
@ -0,0 +1,110 @@ |
|||||||
|
# Created by .ignore support plugin (hsz.mobi) |
||||||
|
### Python template |
||||||
|
# Byte-compiled / optimized / DLL files |
||||||
|
__pycache__/ |
||||||
|
*.py[cod] |
||||||
|
*$py.class |
||||||
|
|
||||||
|
# C extensions |
||||||
|
*.so |
||||||
|
|
||||||
|
# Distribution / packaging |
||||||
|
.Python |
||||||
|
build/ |
||||||
|
develop-eggs/ |
||||||
|
dist/ |
||||||
|
downloads/ |
||||||
|
eggs/ |
||||||
|
.eggs/ |
||||||
|
lib/ |
||||||
|
lib64/ |
||||||
|
parts/ |
||||||
|
sdist/ |
||||||
|
var/ |
||||||
|
wheels/ |
||||||
|
*.egg-info/ |
||||||
|
.installed.cfg |
||||||
|
*.egg |
||||||
|
MANIFEST |
||||||
|
|
||||||
|
# PyInstaller |
||||||
|
# Usually these files are written by a python script from a template |
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it. |
||||||
|
*.manifest |
||||||
|
*.spec |
||||||
|
|
||||||
|
# Installer logs |
||||||
|
pip-log.txt |
||||||
|
pip-delete-this-directory.txt |
||||||
|
|
||||||
|
# Unit test / coverage reports |
||||||
|
htmlcov/ |
||||||
|
.tox/ |
||||||
|
.coverage |
||||||
|
.coverage.* |
||||||
|
.cache |
||||||
|
nosetests.xml |
||||||
|
coverage.xml |
||||||
|
*.cover |
||||||
|
.hypothesis/ |
||||||
|
|
||||||
|
# Translations |
||||||
|
*.mo |
||||||
|
*.pot |
||||||
|
|
||||||
|
# Django stuff: |
||||||
|
*.log |
||||||
|
.static_storage/ |
||||||
|
.media/ |
||||||
|
local_settings.py |
||||||
|
|
||||||
|
# Flask stuff: |
||||||
|
instance/ |
||||||
|
.webassets-cache |
||||||
|
|
||||||
|
# Scrapy stuff: |
||||||
|
.scrapy |
||||||
|
|
||||||
|
# Sphinx documentation |
||||||
|
docs/_build/ |
||||||
|
|
||||||
|
# PyBuilder |
||||||
|
target/ |
||||||
|
|
||||||
|
.idea/ |
||||||
|
|
||||||
|
# Jupyter Notebook |
||||||
|
.ipynb_checkpoints |
||||||
|
|
||||||
|
# pyenv |
||||||
|
.python-version |
||||||
|
|
||||||
|
# celery beat schedule file |
||||||
|
celerybeat-schedule |
||||||
|
|
||||||
|
# SageMath parsed files |
||||||
|
*.sage.py |
||||||
|
|
||||||
|
# Environments |
||||||
|
.env |
||||||
|
.venv |
||||||
|
env/ |
||||||
|
venv/ |
||||||
|
ENV/ |
||||||
|
env.bak/ |
||||||
|
venv.bak/ |
||||||
|
|
||||||
|
# Spyder project settings |
||||||
|
.spyderproject |
||||||
|
.spyproject |
||||||
|
|
||||||
|
# Rope project settings |
||||||
|
.ropeproject |
||||||
|
|
||||||
|
# mkdocs documentation |
||||||
|
/site |
||||||
|
|
||||||
|
# mypy |
||||||
|
.mypy_cache/ |
||||||
|
|
||||||
|
personal.py |
@ -0,0 +1,55 @@ |
|||||||
|
# ProFin - Personal Finance Projector |
||||||
|
|
||||||
|
``` |
||||||
|
__ |
||||||
|
o /' ) |
||||||
|
/' ( , |
||||||
|
__/' PRO ) .' `; |
||||||
|
o _.-~~~~' ``---..__ .' ; |
||||||
|
_.--' b) ``--...____.' .' |
||||||
|
( _. )). `-._ < |
||||||
|
`vvvvvvv-)-.....___.- `-. __...--'-.'. |
||||||
|
`^^^^^'-------.....`-.___.'----... .' `.; |
||||||
|
jgs `-` ` |
||||||
|
``` |
||||||
|
|
||||||
|
ProFin is a python module for projecting the balance of a personal savings account |
||||||
|
over a future period. |
||||||
|
|
||||||
|
The output can be used to visualise the balance development when planning some big |
||||||
|
purchase or a change of the living situation, such as buying a flat or a car, |
||||||
|
getting a new job with possible months of no income, etc. |
||||||
|
|
||||||
|
Please note the script is intended only for orientation and there may be minor |
||||||
|
inaccuracies or bugs (please report!) |
||||||
|
|
||||||
|
## Supported Features |
||||||
|
|
||||||
|
### Financial Operations & Schemes |
||||||
|
|
||||||
|
- Once-off expenses and income |
||||||
|
- Recurrent monthly payments / income at a fixed date |
||||||
|
- Monthly payments with a total expense cap |
||||||
|
- Skipping a month in monthly payments / income |
||||||
|
- Spread monthly expenses (e.g. food cost) - distributed over all days in a month |
||||||
|
- Simple loan (borrowing money from a friend) with back-payments |
||||||
|
- Setting initial or correction balance |
||||||
|
|
||||||
|
### Functions |
||||||
|
|
||||||
|
- Setting up a financial situation with changes at arbitrary dates |
||||||
|
- Simulating account balance with day granularity |
||||||
|
- Plotting results using PyPlot |
||||||
|
|
||||||
|
## Dependencies |
||||||
|
|
||||||
|
The script uses NumPy, MatPlotLib and Pandas for plotting. This may be improved in a future version |
||||||
|
(in particular Pandas could be replaced). |
||||||
|
|
||||||
|
If plotting is not needed and the dependencies are not available (e.g. some old Debian), |
||||||
|
remove their imports and comment out the graph() function |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
See the example files for an example of usage. (The examples are silly and not very representative, but they show the API well). |
||||||
|
Additionally the code is documented with doc comments. |
@ -0,0 +1,53 @@ |
|||||||
|
import profin |
||||||
|
|
||||||
|
pf = profin.Projector() |
||||||
|
|
||||||
|
# This demo simulates Betty who saved for a car |
||||||
|
|
||||||
|
|
||||||
|
# Start date and balance |
||||||
|
pf.date(2018, 'March', 24) |
||||||
|
pf.balance(+40_000) # initial savings |
||||||
|
|
||||||
|
# Recurring payments, paid on 1st of eahc month |
||||||
|
rent = pf.monthly('Rent', -8_000) # a pretty high rent... |
||||||
|
|
||||||
|
# using .spread() to distribute the cost over the whole month |
||||||
|
# Set to False for less accurate graph but more readable stdout logs |
||||||
|
want_spread = True |
||||||
|
|
||||||
|
food = pf.monthly('Food', -3500).spread(want_spread) |
||||||
|
|
||||||
|
# Betty goes to gym and it's very expensive |
||||||
|
gym1 = pf.monthly('Gym 1', -500).on(10) |
||||||
|
gym2 = pf.monthly('Gym 2', -500).on(20) |
||||||
|
|
||||||
|
# She is a cashier but the pay isn't that good |
||||||
|
job = pf.monthly('Cashier Job', +22_000, day=6) |
||||||
|
|
||||||
|
|
||||||
|
pf.date(2018, 'June') |
||||||
|
# Betty is saving for a car, she stopped going to the gym and instead jogs outside. |
||||||
|
gym1.end() |
||||||
|
gym2.end() |
||||||
|
|
||||||
|
|
||||||
|
# In September, Frank lent Betty some cash for the car |
||||||
|
friend_loan = pf.borrow('Frank\'s Loan', +35_000).on(2018, 'Sep')\ |
||||||
|
.repay_monthly(-8000, day=7)\ |
||||||
|
.begin(2018, 'Dec') # Frank gives Betty 3 months before she has to start paying it back |
||||||
|
|
||||||
|
# Betty can't decide which car to buy. Finally... |
||||||
|
pf.expend("Car Purchase", 120_000).on(2018, 'Sep', 28) |
||||||
|
|
||||||
|
# whoops that was expensive. But she'll recover ... |
||||||
|
|
||||||
|
# Now she's paying off the loan. It's done in about 3 months after December |
||||||
|
|
||||||
|
|
||||||
|
# --- We generate the prediction until December 31, 2019 |
||||||
|
# You can pass a second argument verbose=False to disable stdout printing |
||||||
|
samples = pf.project_to(2019) |
||||||
|
|
||||||
|
# show in a graph |
||||||
|
pf.graph(samples) |
@ -0,0 +1,57 @@ |
|||||||
|
import profin |
||||||
|
|
||||||
|
pf = profin.Projector() |
||||||
|
|
||||||
|
# This demo simulates Mr. Bob who gave up one poorly paying job, then had trouble finding a better one, |
||||||
|
# had to borrow from a friend, then finally found a good paying job and all was OK |
||||||
|
|
||||||
|
|
||||||
|
# Start date and balance |
||||||
|
pf.date(2018, 'April', 24) |
||||||
|
pf.balance(+100_000) # initial savings |
||||||
|
|
||||||
|
# Recurring payments, paid on 1st of eahc month |
||||||
|
rent = pf.monthly('Rent', -12_000) # a pretty high rent... |
||||||
|
internet = pf.monthly('Internet', -450).on(10) # this is paid on the 10th |
||||||
|
spotify = pf.monthly('Spotify', -160) |
||||||
|
|
||||||
|
# using .spread() to distribute the cost over the whole month |
||||||
|
# Set to False for less accurate graph but more readable stdout logs |
||||||
|
want_spread = True |
||||||
|
|
||||||
|
food = pf.monthly('Food', -3500).spread(want_spread) |
||||||
|
membership = pf.monthly('Club Member.', -200).spread(want_spread) |
||||||
|
misc = pf.monthly('Misc Expenses', -2000).spread(want_spread) |
||||||
|
|
||||||
|
# A poorly paying job |
||||||
|
job1 = pf.monthly('Cleaning Job', +15_000, day=12) |
||||||
|
|
||||||
|
# Not going to the Club in May |
||||||
|
membership.skip_month(2018, 'May') |
||||||
|
|
||||||
|
# --- we can set a global date instead of specifying it in each start() and end() |
||||||
|
# also, if .start() is not called, the global date is used |
||||||
|
pf.date(2018, 'June') |
||||||
|
|
||||||
|
job1.end() # Quit the job in May, last pay received in June |
||||||
|
|
||||||
|
# cancel non-essential expenses |
||||||
|
spotify.end() |
||||||
|
membership.end() |
||||||
|
misc.end() |
||||||
|
|
||||||
|
# Looking bad, borrow from a friend |
||||||
|
friend_loan = pf.borrow('Loan from Luke', +50_000).on(2018, 'Oct')\ |
||||||
|
.repay_monthly(-5000, day=18) |
||||||
|
|
||||||
|
# A new job with much better pay than before! |
||||||
|
pf.date(2018, 'Dec') |
||||||
|
job2 = pf.monthly('Sausage Stand Job', +25_000, day=15) |
||||||
|
|
||||||
|
|
||||||
|
# --- We generate the prediction until December 31, 2019 |
||||||
|
# You can pass a second argument verbose=False to disable stdout printing |
||||||
|
samples = pf.project_to(2019) |
||||||
|
|
||||||
|
# show in a graph |
||||||
|
pf.graph(samples) |
@ -0,0 +1,373 @@ |
|||||||
|
from calendar import monthrange |
||||||
|
import datetime |
||||||
|
import numpy as np |
||||||
|
import matplotlib.pyplot as plt |
||||||
|
import matplotlib.dates as mdates |
||||||
|
import matplotlib.cbook as cbook |
||||||
|
import pandas as pd |
||||||
|
|
||||||
|
|
||||||
|
def parse_month(month) -> int: |
||||||
|
""" Parse string month to a number 1-12 """ |
||||||
|
map = { |
||||||
|
'jan': 1, |
||||||
|
'feb': 2, |
||||||
|
'mar': 3, |
||||||
|
'apr': 4, |
||||||
|
'may': 5, |
||||||
|
'jun': 6, |
||||||
|
'jul': 7, |
||||||
|
'aug': 8, |
||||||
|
'sep': 9, |
||||||
|
'oct': 10, |
||||||
|
'nov': 11, |
||||||
|
'dec': 12, |
||||||
|
|
||||||
|
'sept': 9, |
||||||
|
|
||||||
|
'january': 1, |
||||||
|
'february': 2, |
||||||
|
'march': 3, |
||||||
|
'april': 4, |
||||||
|
'june': 6, |
||||||
|
'july': 7, |
||||||
|
'august': 8, |
||||||
|
'september': 9, |
||||||
|
'october': 10, |
||||||
|
'november': 11, |
||||||
|
'december': 12 |
||||||
|
} |
||||||
|
|
||||||
|
k = str(month).lower() |
||||||
|
if k in map: |
||||||
|
return map[k] |
||||||
|
|
||||||
|
return int(month) # try it as numeric |
||||||
|
|
||||||
|
class Projector: |
||||||
|
def __init__(self): |
||||||
|
self.incomes = [] |
||||||
|
self.cursor = datetime.datetime.now().date() |
||||||
|
self.oldest = None |
||||||
|
|
||||||
|
def date(self, year:int, month='Jan', day:int=1) -> 'Projector': |
||||||
|
self.cursor = datetime.date(year, parse_month(month), day) |
||||||
|
|
||||||
|
if self.oldest is None: |
||||||
|
self.oldest = self.cursor |
||||||
|
|
||||||
|
return self |
||||||
|
|
||||||
|
def project_to(self, year, month='Dec', day=31, verbose=True): |
||||||
|
records = [] |
||||||
|
|
||||||
|
end = None |
||||||
|
aday = day |
||||||
|
while aday >= 28: |
||||||
|
try: |
||||||
|
end = datetime.date(year, parse_month(month), aday) |
||||||
|
break |
||||||
|
except ValueError: |
||||||
|
aday -= 1 |
||||||
|
|
||||||
|
if end is None: |
||||||
|
raise ValueError("Bad projection end date") |
||||||
|
|
||||||
|
now = self.oldest |
||||||
|
balance = 0 |
||||||
|
while now <= end: |
||||||
|
buf = "" |
||||||
|
any_non_balance = False |
||||||
|
any = False |
||||||
|
|
||||||
|
for inc in self.incomes: |
||||||
|
ain = inc.get_absolute_income_on(now) |
||||||
|
if ain is not None: |
||||||
|
balance = ain |
||||||
|
any = True |
||||||
|
if verbose: |
||||||
|
buf += "\nSet Balance: %d" % balance |
||||||
|
else: |
||||||
|
inco = inc.get_income_on(now) |
||||||
|
if inco is not None and inco != 0: |
||||||
|
any = True |
||||||
|
any_non_balance = True |
||||||
|
balance += inco |
||||||
|
if verbose: |
||||||
|
buf += "\n| %20s ... %s%d" % (inc.name, '+' if inco>0 else '', inco) |
||||||
|
|
||||||
|
if any: |
||||||
|
if verbose: |
||||||
|
print(now) |
||||||
|
print(buf.strip()) |
||||||
|
if any_non_balance: |
||||||
|
print('End Balance: %d' % balance) |
||||||
|
print('\n') |
||||||
|
|
||||||
|
records.append({'date': now, 'balance': balance}) |
||||||
|
|
||||||
|
now += datetime.timedelta(days=1) |
||||||
|
|
||||||
|
return records |
||||||
|
|
||||||
|
def graph(self, samples, currency='CZK'): |
||||||
|
""" Show samples from project_to() in a line graph """ |
||||||
|
years = mdates.YearLocator() # every year |
||||||
|
months = mdates.MonthLocator() # every month |
||||||
|
yearsFmt = mdates.DateFormatter('%Y') |
||||||
|
|
||||||
|
r = pd.DataFrame().from_records(samples) |
||||||
|
|
||||||
|
fig, ax = plt.subplots() |
||||||
|
ax.step(r.date, r.balance, where='post') |
||||||
|
|
||||||
|
# format the ticks |
||||||
|
ax.xaxis.set_major_locator(years) |
||||||
|
ax.xaxis.set_major_formatter(yearsFmt) |
||||||
|
ax.xaxis.set_minor_locator(months) |
||||||
|
|
||||||
|
# round to nearest years... |
||||||
|
datemin = np.datetime64(r.date[0], 'Y') |
||||||
|
datemax = np.datetime64(r.date[len(r.date)-1], 'Y') + np.timedelta64(1, 'Y') |
||||||
|
ax.set_xlim(datemin, datemax) |
||||||
|
|
||||||
|
# format the coords message box |
||||||
|
def price(x): |
||||||
|
return currency+' %1.0f' % x |
||||||
|
|
||||||
|
ax.format_xdata = mdates.DateFormatter('%Y-%m-%d') |
||||||
|
ax.format_ydata = price |
||||||
|
ax.grid(True) |
||||||
|
|
||||||
|
# rotates and right aligns the x labels, and moves the bottom of the |
||||||
|
# axes up to make room for them |
||||||
|
fig.autofmt_xdate() |
||||||
|
|
||||||
|
plt.show() |
||||||
|
|
||||||
|
def balance(self, balance:int) -> 'Projector': |
||||||
|
""" Set known balance at a date cursor (this modifies the total value) """ |
||||||
|
self.incomes.append(SetBalance(self, balance)) |
||||||
|
return self |
||||||
|
|
||||||
|
def monthly(self, name: str, income: int, day=1) -> 'MonthlyIncome': |
||||||
|
""" Monthly recurrent expense or income """ |
||||||
|
m = MonthlyIncome(self, name, per_month=income, payday=day) |
||||||
|
self.incomes.append(m) |
||||||
|
return m |
||||||
|
|
||||||
|
def receive(self, name, money) -> 'SingleIncome': |
||||||
|
""" Once-off Income """ |
||||||
|
return self.single(name, +abs(money)) |
||||||
|
|
||||||
|
def expend(self, name, money) -> 'SingleIncome': |
||||||
|
""" Expense """ |
||||||
|
return self.single(name, -abs(money)) |
||||||
|
|
||||||
|
def single(self, name, money) -> 'SingleIncome': |
||||||
|
""" Non-recurrent expense or income """ |
||||||
|
m = SingleIncome(self, name, money=money) |
||||||
|
self.incomes.append(m) |
||||||
|
return m |
||||||
|
|
||||||
|
def borrow(self, name, money) -> 'SimpleLoan': |
||||||
|
""" Borrow money with no interest, to be repaid """ |
||||||
|
m = SimpleLoan(self, name, money=money) |
||||||
|
self.incomes.append(m) |
||||||
|
return m |
||||||
|
|
||||||
|
|
||||||
|
class AIncome: |
||||||
|
""" Abstract income record """ |
||||||
|
def __init__(self, name:str, pf:Projector): |
||||||
|
self.pf = pf |
||||||
|
self.name = name |
||||||
|
|
||||||
|
self.date_start = pf.cursor |
||||||
|
self.date_end = None |
||||||
|
self.started = False |
||||||
|
self.ended = False |
||||||
|
|
||||||
|
def get_income_on(self, date): |
||||||
|
""" Get income on a given date """ |
||||||
|
|
||||||
|
if self.ended: |
||||||
|
return 0 |
||||||
|
|
||||||
|
if self.date_end is not None and self.date_end <= date: |
||||||
|
self.ended = True |
||||||
|
|
||||||
|
if not self.started and self.date_start <= date: |
||||||
|
self.started = True |
||||||
|
|
||||||
|
if self.started: |
||||||
|
return self._day_income(date) |
||||||
|
|
||||||
|
def _day_income(self, date): |
||||||
|
""" Day's income, end dates have been already checked and are OK """ |
||||||
|
raise NotImplementedError() |
||||||
|
|
||||||
|
def get_absolute_income_on(self, date): |
||||||
|
""" Get absolute income value (used for SetBalance) """ |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
class MonthlyIncome(AIncome): |
||||||
|
""" Periodic income with monthly period """ |
||||||
|
def __init__(self, pf: Projector, name:str, per_month:int, payday:int=1): |
||||||
|
""" |
||||||
|
payday - the day of the month when the money is sent or received |
||||||
|
per_month - money sent or received per month |
||||||
|
""" |
||||||
|
super().__init__(name, pf) |
||||||
|
self.skip_dates = [] |
||||||
|
self.monthly = per_month |
||||||
|
self.payday = payday |
||||||
|
self.remains = None |
||||||
|
self.spreading = False |
||||||
|
|
||||||
|
def on(self, day:int) -> 'MonthlyIncome': |
||||||
|
""" Set the payday """ |
||||||
|
self.payday = day |
||||||
|
return self |
||||||
|
|
||||||
|
def _day_income(self, date:datetime.date) -> int: |
||||||
|
if datetime.date(date.year, date.month, 1) in self.skip_dates: |
||||||
|
return 0 |
||||||
|
|
||||||
|
if self.spreading: |
||||||
|
if self.remains is not None: |
||||||
|
if self.remains > 0: |
||||||
|
if abs(self.monthly) > self.remains: |
||||||
|
self.ended = True |
||||||
|
topay = self.remains |
||||||
|
self.remains = 0 |
||||||
|
return topay if self.monthly > 0 else -topay |
||||||
|
self.remains -= round(abs(self.monthly) / monthrange(date.year, date.month)[1]) |
||||||
|
else: |
||||||
|
return 0 |
||||||
|
|
||||||
|
x = round(self.monthly / monthrange(date.year, date.month)[1]) |
||||||
|
return x |
||||||
|
else: |
||||||
|
if date.day == self.payday: |
||||||
|
if self.remains is not None: |
||||||
|
if self.remains > 0: |
||||||
|
if abs(self.monthly) > self.remains: |
||||||
|
self.ended = True |
||||||
|
topay = self.remains |
||||||
|
self.remains = 0 |
||||||
|
return topay |
||||||
|
self.remains -= abs(self.monthly) |
||||||
|
else: |
||||||
|
return 0 |
||||||
|
return self.monthly |
||||||
|
else: |
||||||
|
return 0 |
||||||
|
|
||||||
|
def skip_month(self, year, month) -> 'MonthlyIncome': |
||||||
|
""" Skip a month """ |
||||||
|
self.skip_dates.append(datetime.date(year, parse_month(month), 1)) |
||||||
|
return self |
||||||
|
|
||||||
|
def start(self, year=None, month=1, day=1) -> 'MonthlyIncome': |
||||||
|
""" Set the start date """ |
||||||
|
if year is None: |
||||||
|
self.date_start = self.pf.cursor |
||||||
|
else: |
||||||
|
self.date_start = datetime.date(year, parse_month(month), day) |
||||||
|
return self |
||||||
|
|
||||||
|
def end(self, year=None, month=1, day=1) -> 'MonthlyIncome': |
||||||
|
""" Set the end date """ |
||||||
|
|
||||||
|
if year is None: |
||||||
|
self.date_end = datetime.date(self.pf.cursor.year, self.pf.cursor.month, |
||||||
|
monthrange(self.pf.cursor.year, self.pf.cursor.month)[1]) # last day of the month |
||||||
|
else: |
||||||
|
self.date_end = datetime.date(year, parse_month(month), day) |
||||||
|
return self |
||||||
|
|
||||||
|
def total(self, total) -> 'MonthlyIncome': |
||||||
|
""" Set the total after which the payment is stopped """ |
||||||
|
self.remains = total |
||||||
|
return self |
||||||
|
|
||||||
|
def spread(self, doSpread=True) -> 'MonthlyIncome': |
||||||
|
""" Spread over the month """ |
||||||
|
self.spreading = doSpread |
||||||
|
return self |
||||||
|
|
||||||
|
|
||||||
|
class SingleIncome(AIncome): |
||||||
|
""" Single-shot income """ |
||||||
|
|
||||||
|
def __init__(self, pf: Projector, name:str, money:int): |
||||||
|
super().__init__(name, pf) |
||||||
|
self.money = money |
||||||
|
self.start = pf.cursor |
||||||
|
self.end = pf.cursor |
||||||
|
|
||||||
|
def _day_income(self, date): |
||||||
|
return self.money |
||||||
|
|
||||||
|
def on(self, year, month=1, day=1) -> 'SingleIncome': |
||||||
|
""" Set the exact date """ |
||||||
|
self.date_start = self.date_end = datetime.date(year, parse_month(month), day) |
||||||
|
return self |
||||||
|
|
||||||
|
|
||||||
|
class SimpleLoan(AIncome): |
||||||
|
""" Simple interest-free loan """ |
||||||
|
def __init__(self, pf: Projector, name:str, money:int): |
||||||
|
""" |
||||||
|
Money is the total borrowed money |
||||||
|
""" |
||||||
|
super().__init__(name, pf) |
||||||
|
self.borrowed = money |
||||||
|
self.start = pf.cursor |
||||||
|
self.end = pf.cursor |
||||||
|
|
||||||
|
self.receipt = pf.receive(name, money) |
||||||
|
self.repay = None |
||||||
|
|
||||||
|
def on(self, year, month=1, day=1) -> 'SimpleLoan': |
||||||
|
""" Set borrow date """ |
||||||
|
self.start = datetime.date(year, parse_month(month), day) |
||||||
|
self.receipt.on(year, month, day) |
||||||
|
return self |
||||||
|
|
||||||
|
def repay_monthly(self, payment, day=1) -> 'SimpleLoan': |
||||||
|
""" |
||||||
|
Configure monthly payments. |
||||||
|
payment - how much to pay monthly |
||||||
|
day - day of the month to pay |
||||||
|
""" |
||||||
|
self.repay = self.pf.monthly(self.name, -abs(payment), day=day).total(abs(self.borrowed)) |
||||||
|
return self.begin(self.start.year, self.start.month) |
||||||
|
|
||||||
|
def begin(self, year, month) -> 'SimpleLoan': |
||||||
|
""" Set the payments start month """ |
||||||
|
self.repay.start(year, month) |
||||||
|
return self |
||||||
|
|
||||||
|
def _day_income(self, date): |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
class SetBalance (AIncome): |
||||||
|
""" Set balance to a known value """ |
||||||
|
def __init__(self, pf: Projector, balance:int): |
||||||
|
super().__init__('Balance', pf) |
||||||
|
self.balance = balance |
||||||
|
self.start = pf.cursor |
||||||
|
self.end = pf.cursor |
||||||
|
|
||||||
|
def _day_income(self, date): |
||||||
|
return 0 |
||||||
|
|
||||||
|
def get_absolute_income_on(self, date): |
||||||
|
if date == self.start: |
||||||
|
return self.balance |
||||||
|
else: |
||||||
|
return None |
Loading…
Reference in new issue