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