home · contact · privacy
Add script to unify all accounting servers into one.
[misc] / income_progress_bars.py
1 import os
2 import json
3 import jinja2
4 from plomlib import PlomDB, PlomException, run_server, PlomHandler 
5
6 server_port = 8083
7 db_path = '/home/plom/org/income.json'
8
9 tmpl = """
10 <style>
11 body {
12   font-family: monospace;
13   background-color: #e0e0ff;
14 }
15 .countable {
16   font-family: monospace;
17   text-align: right;
18 }
19 td, th {
20   border: 1px solid black;
21 }
22 .progressbar {
23   width: 100px;
24   height: 20px;
25   background-color: black;
26   border: none;
27 }
28 .surplusbar {
29   width: 200px;
30 }
31 .time_progress {
32   position: absolute;
33   height: 20px;
34   background-color: white;
35   width: 2px;
36   border-left: 1px solid black;
37   border-right: 1px solid black;
38   z-index: 2;
39 }
40 .progress {
41   height: 20px;
42   z-index: 1;
43 }
44 .surplus {
45   background-color: green;
46 }
47 input {
48   font-family: monospace;
49   text-align: right;
50 }
51 input.rate {
52   text-align: center;
53   width: 4em;
54 }
55 input.minutes {
56   width: 4em;
57 }
58 input.workdays {
59   width: 3em;
60 }
61 input.year_goal {
62   width: 5em;
63 }
64 .diff_goal {
65   position: absolute;
66   width: 7em;
67   text-align: right;
68   color: white;
69   z-index: 3;
70 }
71 table {
72   margin-bottom: 2em;
73 }
74 .input_container {
75   text-align: center;
76 }
77 </style>
78 <body>
79 <table>
80 <tr><th></th><th>earned</th><th>progress</th><th>surplus</th></tr >
81 {% for p in progress_bars %}<tr><th>{{p.title}}</th>
82 <td class="countable">{{p.earned|round(2)}}</td>
83 <td class="progressbar">{% if p.time_progress >= 0 %}<div class="time_progress" style="margin-left: {{p.time_progress}}px"></div>{% endif %}<div class="progress" style="background-color: {% if p.success < 0.5 %}red{% elif p.success < 1 %}yellow{% else %}green{% endif %}; width: {{p.success_income_cut}}px"></div></td>
84 <td class="progressbar surplusbar"><div class="diff_goal">{{p.diff_goal}}</div><div class="progressbar surplus" style="width: {{p.success_income_bonus}}px" ></div></td></tr>
85 {% endfor %}</table>
86
87 <form action="{{homepage}}" method="POST">
88 <table>
89 <tr><th>hourly rate</th><th>worked today</th></tr>
90 <tr><td class="input_container"><input type="number" min="1" class="rate" name="workday_hourly_rate_1" value="{{workday_hourly_rate_1}}"/>€</td><td><input type="number" min="0" class="minutes" name="workday_minutes_worked_1" value="{{workday_minutes_worked_1}}" step="5" /> minutes</td>
91 <tr><td class="input_container"><input type="number" min="1" class="rate" name="workday_hourly_rate_2" value="{{workday_hourly_rate_2}}"/>€</td><td><input type="number" min="0" class="minutes" name="workday_minutes_worked_2" value="{{workday_minutes_worked_2}}" step="5" /> minutes</td>
92 <tr><td class="input_container"><input type="number" min="1" class="rate" name="workday_hourly_rate_3" value="{{workday_hourly_rate_3}}"/>€</td><td><input type="number" min="0" class="minutes" name="workday_minutes_worked_3" value="{{workday_minutes_worked_3}}" step="5" /> minutes</td>
93 </table>
94 <table>
95 <tr><th>yearly income goal</th><td><input type="number" class="year_goal" min="1" name="year_goal" value="{{year_goal}}" />€</td></tr>
96 <tr><th>monthly income goal</th><td class="countable">{{month_goal|round(2)}}€</td></tr>
97 <tr><th>weekly income goal</th><td class="countable">{{week_goal|round(2)}}€</td></tr>
98 <tr><th>workdays per month</th><td class="input_container"><input type="number" class="workdays" min="1" max="28" name="workdays_per_month" value="{{workdays_per_month}}" /></td></tr>
99 <tr><th>workday income goal</th><td class="countable">{{workday_goal|round(2)}}€</td></tr>
100 <tr><th>workdays per week</th><td class="countable">{{workdays_per_week|round(2)}}</td></tr>
101 </table>
102 <input type="submit" name="update" value="update inputs" />
103 <input type="submit" name="finish" value="finish day" />
104 </form>
105 """
106
107
108 class IncomeDB(PlomDB):
109
110     def __init__(self):
111         # defaults
112         self.timestamp_year = 0,
113         self.timestamp_month = 0,
114         self.timestamp_week = 0,
115         self.year_income = 0,
116         self.month_income = 0,
117         self.week_income = 0,
118         self.workday_hourly_rate_1 = 10,
119         self.workday_hourly_rate_2 = 25,
120         self.workday_hourly_rate_3 = 50,
121         self.workday_minutes_worked_1 = 0,
122         self.workday_minutes_worked_2 = 0,
123         self.workday_minutes_worked_3 = 0,
124         self.year_goal = 20000,
125         self.workdays_per_month = 16
126         super().__init__(db_path)
127
128     def read_db_file(self, f):
129         d = json.load(f)
130         for k, v in d.items():
131             if not hasattr(self, k):
132                 raise PlomException("bad key in db: " + k)
133             setattr(self, k, v)
134
135     def to_dict(self):
136         keys = [k for k in dir(self) if (not k.startswith('_')) and (not callable(getattr(self, k)))]
137         d = {}
138         for k in keys:
139             d[k] = getattr(self, k)
140         return d
141
142     def write_db(self):
143         self.write_text_to_db(json.dumps(self.to_dict()))
144
145
146 class ProgressBar:
147     def __init__(self, title, earned, goal, time_progress=-1):
148         self.title = title
149         self.earned = earned
150         self.time_progress = int(time_progress * 100)
151         success_income = self.earned / goal
152         self.success_income_cut = int(min(success_income, 1.0) * 100)
153         self.success_income_bonus = int(max(success_income - 1.0, 0) * 100)
154         self.success = success_income + 0
155         self.diff_goal = "%.2f€" % (self.earned - goal)
156         if title != "workday":
157             self.diff_goal += "(%.2f€)" % (self.earned - (goal * time_progress))
158         if time_progress >= 0:
159             self.success = 1
160             if time_progress > 0:
161                 self.success = success_income / time_progress
162
163
164 class IncomeProgressHandler(PlomHandler):
165     
166     def app_init(self, handler):
167         default_path = '/income_progress'
168         handler.add_route('GET', default_path, self.display_income_progress) 
169         handler.add_route('POST', default_path, self.post_income_update) 
170         return 'income_progress', default_path 
171
172     def do_POST(self):
173         self.post_income_update()
174
175     def post_income_update(self):
176         from urllib.parse import parse_qs
177         try:
178             length = int(self.headers['content-length'])
179             postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
180             db = IncomeDB()
181             db.workday_minutes_worked_1 = int(postvars['workday_minutes_worked_1'][0])
182             db.workday_minutes_worked_2 = int(postvars['workday_minutes_worked_2'][0])
183             db.workday_minutes_worked_3 = int(postvars['workday_minutes_worked_3'][0])
184             db.workday_hourly_rate_1 = int(postvars['workday_hourly_rate_1'][0])
185             db.workday_hourly_rate_2 = int(postvars['workday_hourly_rate_2'][0])
186             db.workday_hourly_rate_3 = int(postvars['workday_hourly_rate_3'][0])
187             db.year_goal = int(postvars['year_goal'][0])
188             db.workdays_per_month = int(postvars['workdays_per_month'][0])
189             if 'finish' in postvars.keys():
190                 day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1
191                 day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2
192                 day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3
193                 db.year_income += day_income
194                 db.month_income += day_income
195                 db.week_income += day_income
196                 db.workday_minutes_worked_1 = 0
197                 db.workday_minutes_worked_2 = 0
198                 db.workday_minutes_worked_3 = 0
199             db.write_db()
200             homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage
201             self.redirect(homepage)
202         except PlomException as e:
203             self.fail_400(e) 
204
205     def do_GET(self):
206         self.display_income_progress()
207
208     def display_income_progress(self):
209         import datetime
210         import calendar
211         try:
212             db = IncomeDB()
213             today = datetime.datetime.now()
214             update_db = False
215             if today.year != db.timestamp_year:
216                 db.timestamp_year = today.year
217                 db.timestamp_month = today.month
218                 db.year_income = 0
219                 db.month_income = 0
220                 update_db = True
221             if today.month != db.timestamp_month:
222                 db.timestamp_month = today.month
223                 db.month_income = 0
224                 update_db = True
225             if today.isocalendar()[1] != db.timestamp_week:
226                 db.timestamp_week = today.isocalendar()[1]
227                 db.week_income = 0
228                 update_db = True
229             if update_db:
230                 print("Resetting timestamp")
231                 db.write_db()
232             day_of_year = today.toordinal() - datetime.date(today.year, 1, 1).toordinal() + 1
233             year_length = 365 + calendar.isleap(today.year)
234             workday_goal = db.year_goal / 12 / db.workdays_per_month
235             workdays_per_week = (db.workdays_per_month * 12) / (year_length / 7)
236             month_goal = db.year_goal / 12
237             week_goal = db.year_goal / (year_length / 7)
238             day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1
239             day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2
240             day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3
241             year_plus = db.year_income + day_income
242             month_plus = db.month_income + day_income
243             week_plus = db.week_income + day_income
244             progress_time_year = day_of_year / year_length
245             progress_time_month = today.day / calendar.monthrange(today.year, today.month)[1]
246             progress_time_week = today.weekday() / 7
247             progress_bars = [ProgressBar("year", year_plus, db.year_goal, progress_time_year),
248                              ProgressBar("month", month_plus, month_goal, progress_time_month),
249                              ProgressBar("week", week_plus, week_goal, progress_time_week),
250                              ProgressBar("workday", day_income, workday_goal)]
251             homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage
252             page = jinja2.Template(tmpl).render(
253                     homepage = homepage,
254                     progress_bars = progress_bars,
255                     workday_hourly_rate_1 = db.workday_hourly_rate_1,
256                     workday_minutes_worked_1 = db.workday_minutes_worked_1,
257                     workday_hourly_rate_2 = db.workday_hourly_rate_2,
258                     workday_minutes_worked_2 = db.workday_minutes_worked_2,
259                     workday_hourly_rate_3 = db.workday_hourly_rate_3,
260                     workday_minutes_worked_3 = db.workday_minutes_worked_3,
261                     year_goal = db.year_goal,
262                     month_goal = month_goal,
263                     week_goal = week_goal,
264                     workdays_per_month = db.workdays_per_month,
265                     workday_goal = workday_goal,
266                     workdays_per_week = workdays_per_week,
267                     )
268             self.send_HTML(page)
269         except PlomException as e:
270             self.fail_400(e) 
271
272
273 if __name__ == "__main__":       
274     run_server(server_port, IncomeProgressHandler)