Passed
Push — master ( bfb52c...d38334 )
by William
03:48
created

app.cashflow   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 281
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 237
dl 0
loc 281
rs 5.04
c 0
b 0
f 0
wmc 57

4 Functions

Rating   Name   Duplication   Size   Complexity  
B update_cash() 0 49 7
B plot_cash() 0 37 6
F calc_schedule() 0 123 33
C calc_transactions() 0 49 11

How to fix   Complexity   

Complexity

Complex classes like app.cashflow often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
from app import db
2
from .models import Schedule, Total, Running, Transactions, Skip, Balance
3
from datetime import datetime, date
4
import pandas as pd
5
import json
6
import plotly
7
import plotly.express as px
8
import os
9
from dateutil.relativedelta import relativedelta
10
from natsort import index_natsorted
11
import numpy as np
12
import decimal
13
import plotly.graph_objs as go
14
from pathlib import Path
15
16
17
def update_cash(balance, refresh):
18
    # if the database has been modified, update the calculations
19
    try:
20
        modifiedtime = os.path.getmtime(os.environ.get('DATABASE_URL').replace('sqlite:///', ''))
21
        modifiedtime = datetime.fromtimestamp(modifiedtime)
22
        modpath = os.environ.get('DATABASE_URL').replace('sqlite:///', '')
23
        modpath = modpath.replace('db.sqlite', 'modified')
24
        os.close(os.open(modpath, os.O_CREAT))
25
        dbmodified = os.path.getmtime(modpath)
26
        dbmodified = datetime.fromtimestamp(dbmodified)
27
    except:
28
        basedir = os.path.abspath(os.path.dirname(__file__))
29
        datafile = os.path.join(basedir, "data/db.sqlite")
30
        modifiedtime = os.path.getmtime(datafile)
31
        modifiedtime = datetime.fromtimestamp(modifiedtime)
32
        modpath = os.path.join(basedir, "data/modified")
33
        os.close(os.open(modpath, os.O_CREAT))
34
        dbmodified = os.path.getmtime(modpath)
35
        dbmodified = datetime.fromtimestamp(dbmodified)
36
37
    dt = date.today()
38
    today = datetime.combine(dt, datetime.min.time())
39
40
    if modifiedtime > dbmodified or dbmodified < today or refresh == 1:
41
        try:
42
            if balance.amount:
43
                db.session.query(Balance).delete()
44
                balance = Balance(amount=balance.amount, date=datetime.today())
45
                db.session.add(balance)
46
                db.session.commit()
47
        except:
48
            balance = Balance(amount='0',
49
                              date=datetime.today())
50
            db.session.add(balance)
51
            db.session.commit()
52
53
        # empty the tables to create fresh data from the schedule
54
        db.session.query(Total).delete()
55
        db.session.query(Running).delete()
56
        db.session.query(Transactions).delete()
57
        db.session.commit()
58
59
        # calculate total events for the year amount
60
        calc_schedule()
61
62
        # calculate sum of running transactions
63
        calc_transactions(balance)
64
65
        Path(modpath).touch()
66
67
68
def calc_schedule():
69
    months = 13
70
    weeks = 53
71
    years = 1
72
    quarters = 4
73
    biweeks = 27
74
75
    try:
76
        engine = db.create_engine(os.environ.get('DATABASE_URL')).connect()
77
    except:
78
        engine = db.create_engine('sqlite:///db.sqlite').connect()
79
80
    # pull the schedule information
81
    df = pd.read_sql('SELECT * FROM schedule;', engine)
82
83
    # loop through the schedule and create transactions in a table out to the future number of years
84
    todaydate = datetime.today().date()
85
    for i in range(len(df.index)):
86
        format = '%Y-%m-%d'
87
        name = df['name'][i]
88
        startdate = df['startdate'][i]
89
        firstdate = df['firstdate'][i]
90
        frequency = df['frequency'][i]
91
        amount = df['amount'][i]
92
        type = df['type'][i]
93
        existing = Schedule.query.filter_by(name=name).first()
94
        if not firstdate:
95
            existing.firstdate = datetime.strptime(startdate, format).date()
96
            db.session.commit()
97
        if frequency == 'Monthly':
98
            for k in range(months):
99
                futuredate = datetime.strptime(startdate, format).date() + relativedelta(months=k)
100
                futuredateday = futuredate.day
101
                firstdateday = datetime.strptime(firstdate, format).date().day
102
                if firstdateday > futuredateday:
103
                    try:
104
                        for m in range(3):
105
                            futuredateday += 1
106
                            if firstdateday >= futuredateday:
107
                                futuredate = futuredate.replace(day=futuredateday)
108
                    except ValueError:
109
                        pass
110
                if futuredate <= todaydate:
111
                    existing.startdate = futuredate + relativedelta(months=1)
112
                    daycheckdate = futuredate + relativedelta(months=1)
113
                    daycheck = daycheckdate.day
114
                    if firstdateday > daycheck:
115
                        try:
116
                            for m in range(3):
117
                                daycheck += 1
118
                                if firstdateday >= daycheck:
119
                                    existing.startdate = daycheckdate.replace(day=daycheck)
120
                        except ValueError:
121
                            pass
122
                if type == 'Income':
123
                    rollbackdate = datetime.combine(futuredate, datetime.min.time())
124
                    total = Total(type=type, name=name, amount=amount,
125
                                  date=pd.tseries.offsets.BDay(1).rollback(rollbackdate).date())
126
                else:
127
                    total = Total(type=type, name=name, amount=amount, date=futuredate - pd.tseries.offsets.BDay(0))
128
                db.session.add(total)
129
        elif frequency == 'Weekly':
130
            for k in range(weeks):
131
                futuredate = datetime.strptime(startdate, format).date() + relativedelta(weeks=k)
132
                if futuredate <= todaydate:
133
                    existing.startdate = futuredate + relativedelta(weeks=1)
134
                total = Total(type=type, name=name, amount=amount, date=futuredate - pd.tseries.offsets.BDay(0))
135
                db.session.add(total)
136
        elif frequency == 'Yearly':
137
            for k in range(years):
138
                futuredate = datetime.strptime(startdate, format).date() + relativedelta(years=k)
139
                if futuredate <= todaydate:
140
                    existing.startdate = futuredate + relativedelta(years=1)
141
                total = Total(type=type, name=name, amount=amount, date=futuredate - pd.tseries.offsets.BDay(0))
142
                db.session.add(total)
143
        elif frequency == 'Quarterly':
144
            for k in range(quarters):
145
                futuredate = datetime.strptime(startdate, format).date() + relativedelta(months=3 * k)
146
                if futuredate <= todaydate:
147
                    existing.startdate = futuredate + relativedelta(months=3)
148
                total = Total(type=type, name=name, amount=amount, date=futuredate - pd.tseries.offsets.BDay(0))
149
                db.session.add(total)
150
        elif frequency == 'BiWeekly':
151
            for k in range(biweeks):
152
                futuredate = datetime.strptime(startdate, format).date() + relativedelta(weeks=2 * k)
153
                if futuredate <= todaydate:
154
                    existing.startdate = futuredate + relativedelta(weeks=2)
155
                total = Total(type=type, name=name, amount=amount, date=futuredate - pd.tseries.offsets.BDay(0))
156
                db.session.add(total)
157
        elif frequency == 'Onetime':
158
            futuredate = datetime.strptime(startdate, format).date()
159
            if futuredate < todaydate:
160
                db.session.delete(existing)
161
            else:
162
                total = Total(type=type, name=name, amount=amount, date=futuredate)
163
                db.session.add(total)
164
    db.session.commit()
165
166
    # add the hold items
167
    df = pd.read_sql('SELECT * FROM hold;', engine)
168
    for i in range(len(df.index)):
169
        name = df['name'][i]
170
        amount = df['amount'][i]
171
        type = df['type'][i]
172
        total = Total(type=type, name=name, amount=amount, date=todaydate + relativedelta(days=1))
173
        db.session.add(total)
174
    db.session.commit()
175
176
    # add the skip items
177
    df = pd.read_sql('SELECT * FROM skip;', engine)
178
    for i in range(len(df.index)):
179
        format = '%Y-%m-%d'
180
        name = df['name'][i]
181
        amount = df['amount'][i]
182
        type = df['type'][i]
183
        date = df['date'][i]
184
        if datetime.strptime(date, format).date() < todaydate:
185
            skip = Skip.query.filter_by(name=name).first()
186
            db.session.delete(skip)
187
        else:
188
            total = Total(type=type, name=name, amount=amount, date=datetime.strptime(date, format).date())
189
            db.session.add(total)
190
    db.session.commit()
191
192
193
def calc_transactions(balance):
194
    try:
195
        engine = db.create_engine(os.environ.get('DATABASE_URL')).connect()
196
    except:
197
        engine = db.create_engine('sqlite:///db.sqlite').connect()
198
199
    # retrieve the total future transactions
200
    df = pd.read_sql('SELECT * FROM total;', engine)
201
    df = df.sort_values(by="date", key=lambda x: np.argsort(index_natsorted(df["date"])))
202
203
    # collect the next 60 days of transactions for the transactions table
204
    format = '%Y-%m-%d'
205
    todaydate = datetime.today().date()
206
    todaydateplus = todaydate + relativedelta(months=2)
207
    for i in df.iterrows():
208
        if todaydateplus > \
209
                datetime.strptime(i[1].date, format).date() > todaydate and "(SKIP)" not in i[1].iloc[3]:
210
            transactions = Transactions(name=i[1].iloc[3], type=i[1].type, amount=i[1].amount,
211
                                        date=datetime.strptime(i[1].date, format).date())
212
            db.session.add(transactions)
213
    db.session.commit()
214
215
    # for schedules marked as expenses, make the value negative for the sum
216
    for i in df.iterrows():
217
        id = i[1].id
218
        amount = i[1].amount
219
        type = i[1].type
220
        if type == 'Expense':
221
            amount = float(amount) * -1
222
            df.at[id - 1, 'amount'] = amount
223
        elif type == 'Income':
224
            pass
225
226
    # group total transactions by date and sum the amounts for each date
227
    df = df.groupby("date")['amount'].sum().reset_index()
228
229
    # loop through the total transactions by date and add the sums to the total balance amount
230
    runbalance = float(balance.amount)
231
    running = Running(amount=runbalance, date=datetime.today().date())
232
    db.session.add(running)
233
    for i in df.iterrows():
234
        format = '%Y-%m-%d'
235
        rundate = i[1].date
236
        amount = i[1].amount
237
        if datetime.strptime(rundate, format).date() > todaydate:
238
            runbalance += amount
239
            running = Running(amount=runbalance, date=datetime.strptime(rundate, format).date())
240
            db.session.add(running)
241
    db.session.commit()
242
243
244
def plot_cash():
245
    try:
246
        engine = db.create_engine(os.environ.get('DATABASE_URL')).connect()
247
    except:
248
        engine = db.create_engine('sqlite:///db.sqlite').connect()
249
250
    # plot the running balances by date on a line plot
251
    df = pd.read_sql('SELECT * FROM running;', engine)
252
    df = df.sort_values(by='date', ascending=False)
253
    format = '%Y-%m-%d'
254
    minbalance = df['amount'].min()
255
    minbalance = decimal.Decimal(str(minbalance)).quantize(decimal.Decimal('.01'))
256
    if float(minbalance) >= 0:
257
        minrange = 0
258
    else:
259
        minrange = float(minbalance) * 1.1
260
    maxbalance = 0
261
    todaydate = datetime.today().date()
262
    todaydateplus = todaydate + relativedelta(months=2)
263
    for i in df.iterrows():
264
        if todaydateplus > datetime.strptime(i[1].date, format).date() > todaydate:
265
            if i[1].amount > maxbalance:
266
                maxbalance = i[1].amount
267
    maxrange = maxbalance * 1.1
268
    start_date = str(datetime.today().date())
269
    end_date = str(datetime.today().date() + relativedelta(months=2))
270
    layout = go.Layout(yaxis=dict(range=[minrange, maxrange]), xaxis=dict(range=[start_date, end_date]),
271
                       margin=dict(l=5, r=20, t=35, b=5), dragmode='pan')
272
    fig = px.line(df, x="date", y="amount", template="plotly", title="Cash Flow", line_shape="spline")
273
    fig.update_layout(layout)
274
    fig.update_xaxes(title_text='Date')
275
    fig.update_yaxes(title_text='Amount')
276
    fig.update_layout(paper_bgcolor="PaleTurquoise")
277
278
    graphJSON = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
279
280
    return minbalance, graphJSON