From bcdd847ad452aedb769c63b562c32fb577ea190e Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 16:24:21 -0300 Subject: [PATCH 01/35] add requirements.txt including notify to gnome --- requirements.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6c6cce0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +get==2019.4.13 +post==2019.4.13 +public==2019.4.13 +query-string==2019.4.13 +request==2019.4.13 +ruamel.yaml==0.16.10 +ruamel.yaml.clib==0.2.0 +vext==0.7.3 +vext.gi==0.7.0 From c7f0dfb5a37061bdc6927550a60cce13d1f15030 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 16:24:58 -0300 Subject: [PATCH 02/35] add notification --- run.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/run.py b/run.py index 8e9b08c..6bb7130 100644 --- a/run.py +++ b/run.py @@ -22,6 +22,11 @@ parser_since = subparsers.add_parser('since', help='since help') parser_since.add_argument('since', help='since e.g. 2016.08.01') + parser.add_argument( + '--notify', + action="store_true", + ) + args = parser.parse_args() client = togglore.Togglore() @@ -48,5 +53,24 @@ print("Hours worked: {0:.2f}h ({1:.2f} days)".format(actual, actual/client.cfg.work_hours_per_day)) difference = actual-expected - print("Difference: {0:.2f}h ({1:.2f} days)".format(difference, difference/client.cfg.work_hours_per_day)) + output_result = ( + ("Hours to do: {0:.2f}h ({1:.2f} days)".format(expected, expected/client.cfg.work_hours_per_day)) + "\r\n" + + ("Hours worked: {0:.2f}h ({1:.2f} days)".format(actual, actual/client.cfg.work_hours_per_day)) + "\r\n" + + ("Difference: {0:.2f}h ({1:.2f} days)".format(difference, difference/client.cfg.work_hours_per_day)) + ) + print(output_result) + + print(f"Send notification when time is over: {'On' if args.notify else 'Off'}") + if args.notify and difference > 0: + from gi import require_version + require_version('Notify', '0.7') + from gi.repository import Notify + Notify.init("Toggle Notifier") + notification=Notify.Notification.new( + f'Time to stop working (+{difference:.2f}h)', + ('-' * 112) + "\r\n" + output_result, + "dialog-information" + ) + notification.set_timeout(0) + notification.show () From dd32711deefc374620c7199d334a42eb853d5a92 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 16:26:31 -0300 Subject: [PATCH 03/35] include project filter --- togglore/__init__.py | 2 +- togglore/config.py | 6 ++++-- togglore/toggl.py | 27 +++++++++++++++++++++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/togglore/__init__.py b/togglore/__init__.py index ddeaa62..2fcd257 100644 --- a/togglore/__init__.py +++ b/togglore/__init__.py @@ -10,7 +10,7 @@ def __init__(self): config_path = os.path.join(os.path.expanduser('~'), '.togglore') self.cfg = config.Config.read_from_file(config_path) - self.toggle = toggl.TogglClient(self.cfg.api_key, self.cfg.user_id, self.cfg.workspace) + self.toggle = toggl.TogglClient(self.cfg.api_key, self.cfg.user_id, self.cfg.workspace, self.cfg.project) self.time_calculator = utils.WorkTimeCalculator(work_hours_per_day=self.cfg.work_hours_per_day, excluded_days=self.cfg.excluded_days) def diff(self, date_range): diff --git a/togglore/config.py b/togglore/config.py index 8db02cd..6095842 100644 --- a/togglore/config.py +++ b/togglore/config.py @@ -3,12 +3,13 @@ class Config(object): - def __init__(self, api_key=None, work_hours_per_day=8.4, excluded_days=[], user_id=1, workspace=1): + def __init__(self, api_key=None, work_hours_per_day=8.4, excluded_days=[], user_id=1, workspace=1, project=1): self.api_key = api_key self.work_hours_per_day = work_hours_per_day self.excluded_days = excluded_days self.user_id = user_id self.workspace = workspace + self.project = project def write_to_file(self, path): cfg = configparser.ConfigParser() @@ -29,6 +30,7 @@ def read_from_file(cls, path): excluded_days_string = cfg['Work Hours']['excluded_days'] user_id = cfg['User Info']['id'] workspace = cfg['User Info']['workspace'] + project = cfg['User Info']['project'] day_strings = excluded_days_string.split(',') days = [] @@ -37,4 +39,4 @@ def read_from_file(cls, path): days.append(datetime.datetime.strptime(day_string, "%Y.%m.%d").date()) return cls(api_key=api_key, work_hours_per_day=float(work_hours), excluded_days=days, user_id=user_id, - workspace=workspace) + workspace=workspace, project=project) diff --git a/togglore/toggl.py b/togglore/toggl.py index d6bcd20..132f197 100644 --- a/togglore/toggl.py +++ b/togglore/toggl.py @@ -6,10 +6,11 @@ class TogglClient(object): - def __init__(self, api_token, user_id, workspace): + def __init__(self, api_token, user_id, workspace, project): self.api_token = api_token self.user_id = user_id self.workspace = workspace + self.project = project self.headers = {} self.__init_headers() @@ -43,9 +44,15 @@ def time_entries(self, date_range): while current_page * per_page < total: current_page += 1 - response = self.request( - 'https://toggl.com/reports/api/v2/details?workspace_id={}&since={}&until={}&user_agent=togglore&page={}'.format( - self.workspace, date_range.start.isoformat(), date_range.end.isoformat(), current_page)) + url = self._get_toggl_url( + workspace_id=self.workspace, + project_id=self.project, + since=date_range.start.isoformat(), + until=date_range.end.isoformat(), + user_agent='togglore', + page=current_page, + ) + response = self.request(url) total = response['total_count'] per_page = response['per_page'] for time in response['data']: @@ -54,3 +61,15 @@ def time_entries(self, date_range): return entries + + def _get_toggl_url(self, workspace_id, project_id, since, until, user_agent, page): + url = ( + f"https://toggl.com/reports/api/v2/details?" + + f"workspace_id={workspace_id}&" + f"project_ids={project_id}&" + f"since={since}&" + f"until={until}&" + f"user_agent={user_agent}&" + f"page={page}" + ) + return url From 10a11b89093437bb384f8895e364cce38e232d74 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 16:26:51 -0300 Subject: [PATCH 04/35] include runing time entry --- run.py | 10 ++++------ togglore/__init__.py | 5 ++++- togglore/toggl.py | 10 ++++++++++ togglore/utils.py | 5 +++++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/run.py b/run.py index 6bb7130..61abda3 100644 --- a/run.py +++ b/run.py @@ -37,20 +37,18 @@ if args.command == 'range': actual, expected = client.diff(utils.DateRange.parse_from_iso_strings(args.from_date, args.to_date)) elif args.command == 'thisyear': - actual, expected = client.diff(utils.DateRange.this_year()) + actual, expected = client.diff(utils.DateRange.this_year(), include_running=True) elif args.command == 'thismonth': - actual, expected = client.diff(utils.DateRange.this_month()) + actual, expected = client.diff(utils.DateRange.this_month(), include_running=True) elif args.command == 'thisweek': - actual, expected = client.diff(utils.DateRange.this_week()) + actual, expected = client.diff(utils.DateRange.this_week(), include_running=True) elif args.command == 'today': - actual, expected = client.diff(utils.DateRange.today()) + actual, expected = client.diff(utils.DateRange.today(), include_running=True) elif args.command == 'month': actual, expected = client.diff(utils.DateRange.month(int(args.month))) elif args.command == 'since': actual, expected = client.diff(utils.DateRange.since(args.since)) - print("Hours to do: {0:.2f}h ({1:.2f} days)".format(expected, expected/client.cfg.work_hours_per_day)) - print("Hours worked: {0:.2f}h ({1:.2f} days)".format(actual, actual/client.cfg.work_hours_per_day)) difference = actual-expected output_result = ( diff --git a/togglore/__init__.py b/togglore/__init__.py index 2fcd257..ad1862d 100644 --- a/togglore/__init__.py +++ b/togglore/__init__.py @@ -13,8 +13,11 @@ def __init__(self): self.toggle = toggl.TogglClient(self.cfg.api_key, self.cfg.user_id, self.cfg.workspace, self.cfg.project) self.time_calculator = utils.WorkTimeCalculator(work_hours_per_day=self.cfg.work_hours_per_day, excluded_days=self.cfg.excluded_days) - def diff(self, date_range): + def diff(self, date_range, include_running=False): actual_hours = utils.sum_time_of_entries(self.toggle.time_entries(date_range)) expected_hours = self.time_calculator.time_to_work_in_range(date_range) + if include_running: + running_time_entry_hours = utils.get_time_of_running_entry(self.toggle.running_time_entry()) + actual_hours = actual_hours + running_time_entry_hours return actual_hours, expected_hours diff --git a/togglore/toggl.py b/togglore/toggl.py index 132f197..f0a1253 100644 --- a/togglore/toggl.py +++ b/togglore/toggl.py @@ -61,6 +61,16 @@ def time_entries(self, date_range): return entries + def running_time_entry(self): + entry = None + url = "https://www.toggl.com/api/v8/time_entries/current" + response = self.request(url) + if (str(response['data']['wid']) == self.workspace and + str(response['data']['uid']) == self.user_id and + str(response['data']['pid']) == self.project): + entry = response['data'] + return entry + def _get_toggl_url(self, workspace_id, project_id, since, until, user_agent, page): url = ( diff --git a/togglore/utils.py b/togglore/utils.py index 28de140..1cb4564 100644 --- a/togglore/utils.py +++ b/togglore/utils.py @@ -10,6 +10,11 @@ def sum_time_of_entries(entries): return ms / 3600000.0 +def get_time_of_running_entry(entry): + current_time = int(datetime.datetime.now().strftime("%s")) + duration = int(entry['duration']) + return (current_time + duration) / 3600.0 + class WorkTimeCalculator(object): def __init__(self, work_hours_per_day=8.4, excluded_days=[]): From 3c2a45a5dfcc89e87304aca056577403c19ddf49 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 16:26:56 -0300 Subject: [PATCH 05/35] vscode settings --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..09029eb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "venv/bin/python3.6" +} \ No newline at end of file From 957f0df04cd6ce3469d65d18d75be05871a4871d Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 16:58:33 -0300 Subject: [PATCH 06/35] fix no running entry error --- run.py | 4 ++-- togglore/__init__.py | 2 +- togglore/toggl.py | 2 ++ togglore/utils.py | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/run.py b/run.py index 61abda3..e66dbd9 100644 --- a/run.py +++ b/run.py @@ -59,7 +59,7 @@ print(output_result) print(f"Send notification when time is over: {'On' if args.notify else 'Off'}") - if args.notify and difference > 0: + if args.notify: from gi import require_version require_version('Notify', '0.7') from gi.repository import Notify @@ -69,6 +69,6 @@ ('-' * 112) + "\r\n" + output_result, "dialog-information" ) - notification.set_timeout(0) + notification.set_timeout(0) # persist notification.show () diff --git a/togglore/__init__.py b/togglore/__init__.py index ad1862d..506e883 100644 --- a/togglore/__init__.py +++ b/togglore/__init__.py @@ -7,7 +7,7 @@ class Togglore(object): def __init__(self): - config_path = os.path.join(os.path.expanduser('~'), '.togglore') + config_path = os.path.join("/home/italo/", '.togglore') self.cfg = config.Config.read_from_file(config_path) self.toggle = toggl.TogglClient(self.cfg.api_key, self.cfg.user_id, self.cfg.workspace, self.cfg.project) diff --git a/togglore/toggl.py b/togglore/toggl.py index f0a1253..4ba167a 100644 --- a/togglore/toggl.py +++ b/togglore/toggl.py @@ -65,6 +65,8 @@ def running_time_entry(self): entry = None url = "https://www.toggl.com/api/v8/time_entries/current" response = self.request(url) + if response['data'] == None: + return None if (str(response['data']['wid']) == self.workspace and str(response['data']['uid']) == self.user_id and str(response['data']['pid']) == self.project): diff --git a/togglore/utils.py b/togglore/utils.py index 1cb4564..aa4a913 100644 --- a/togglore/utils.py +++ b/togglore/utils.py @@ -11,6 +11,8 @@ def sum_time_of_entries(entries): return ms / 3600000.0 def get_time_of_running_entry(entry): + if not entry: + return 0.0 current_time = int(datetime.datetime.now().strftime("%s")) duration = int(entry['duration']) return (current_time + duration) / 3600.0 From 5e00cb4259815d72381d2b62ac18c98b9135c2a7 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 17:29:38 -0300 Subject: [PATCH 07/35] uses user relative path --- togglore/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/togglore/__init__.py b/togglore/__init__.py index 506e883..ad1862d 100644 --- a/togglore/__init__.py +++ b/togglore/__init__.py @@ -7,7 +7,7 @@ class Togglore(object): def __init__(self): - config_path = os.path.join("/home/italo/", '.togglore') + config_path = os.path.join(os.path.expanduser('~'), '.togglore') self.cfg = config.Config.read_from_file(config_path) self.toggle = toggl.TogglClient(self.cfg.api_key, self.cfg.user_id, self.cfg.workspace, self.cfg.project) From 50ed13fe186c4d6f5485534df39a42f65dbc8684 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 17:45:02 -0300 Subject: [PATCH 08/35] uses notify send option --- run.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/run.py b/run.py index e66dbd9..33ee1c3 100644 --- a/run.py +++ b/run.py @@ -27,6 +27,11 @@ action="store_true", ) + parser.add_argument( + '--uses_notify_send', + action="store_true", + ) + args = parser.parse_args() client = togglore.Togglore() @@ -59,7 +64,8 @@ print(output_result) print(f"Send notification when time is over: {'On' if args.notify else 'Off'}") - if args.notify: + print(f"Uses notify send when time is over: {'On' if args.uses_notify_send else 'Off'}") + if args.notify and difference >= 0: from gi import require_version require_version('Notify', '0.7') from gi.repository import Notify @@ -72,3 +78,12 @@ notification.set_timeout(0) # persist notification.show () + if args.uses_notify_send and difference >= 0: + import os + title = f'Time to stop working (+{difference:.2f}h)' + os.system( + "notify-send \"" + title + "\" " + " \"" + + output_result + "\"" + ) + + From 1e9d5d17c85e421570efe9b6bc8c83980bbfccf0 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 18:20:41 -0300 Subject: [PATCH 09/35] update readme --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++---- docs/image0.png | Bin 0 -> 8691 bytes docs/image1.png | Bin 0 -> 9848 bytes 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 docs/image0.png create mode 100644 docs/image1.png diff --git a/README.md b/README.md index 7495891..6fc2210 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,19 @@ # togglore + Tool for the timetracker [toggle](http://toggl.com/) to calculate the difference between tracked time and the time you should have worked in a given range. ## Setup -Create a config file and save it at ~/.togglore. +Create a virtual env using python3 and install the requirements. + +```sh +virtualenv --python=python3 venv +source venv/bin/activate +pip install -r requirements.txt +``` + +Create a config file and save it at `~/.togglore`. + ```sh [Authentication] API_KEY = 5b9f5e3fd7745a022781daf205f62c72 @@ -13,11 +23,37 @@ hours_per_day = 8.4 excluded_days = 2016.01.01 [User Info] -id = 1 -workspace = 1 +id = 123123123 +workspace = 123123123 +project = 123123123 +``` + +Where to find your information: + +* **API_KEY**: You can find in the section "API Token" in your [profile page](https://toggl.com/app/profile). + + +* **id**, **workspace** and **project**: Open your reports summary in the toggl web app ([link](https://toggl.com/app/reports/summary/)). Then select a filter by Team (select your user) and by project (select the project you want to follow) as showed in the images. Copy the ids showed in the path: `https://toggl.com/app/reports/summary//period/thisWeek/projects//users/` + +![toggl](docs/image0.png) + +![toggl](docs/image1.png) + +## Running notifications + +Open your crontab file (`crontab -e` in a terminal). +Then add the following line in your crontab file: + +```bash +*/10 * * * * eval "export $(egrep -z DBUS_SESSION_BUS_ADDRESS /proc/$(pgrep -u $LOGNAME gnome-session)/environ)" && ~/Documents/GitHub/togglore/venv/bin/python ~/Documents/GitHub/togglore/run.py --notify today ``` +This will run the script with the notify option for today. The first part of the command is used to allow the use of notifications in gnome env when a script is called by crontab. + +Obs: You should add to the crontab of your user, if you insists and add this to the sudo crontab you have to fix the HOME path. + ## Run + ```sh # show diff for today python3 run.py today @@ -37,8 +73,10 @@ python3 run.py month 08 # show diff from 2016.08.01 until today python3 run.py since 2016.08.01 ``` + The output is something like: -``` + +```text Hours to do: 176.00h (22.00 days) Hours worked: 186.65h (23.33 days) Difference: 10.65h (1.33 days) diff --git a/docs/image0.png b/docs/image0.png new file mode 100644 index 0000000000000000000000000000000000000000..07316907411d8cc5f4e8591de0dc5daf206822e3 GIT binary patch literal 8691 zcmb_?WmFtb^d$+wA%x%_JVAropg}?)KyU_k*TFTodw}5X7GQwj5ZnfL28Y23796&J z`(Z!t?)lF-Gu=Jir|MPpd-uJ2t0I&YWpOacFp-duaO6HqsQ^b9@coO93jBU{&>;m5 zuOO0gYUsen2i-IrxF&Ux)^bsGFn4h`ayCPd>ZRmqGx^?T3bJ-ey}=wvRd5OMf8cK-x}kcSfJd@ ztG-j}hmQeB8}Z>{t8t_j3NS?GMXgAUk6)&eA|soA+YOauIA=uK9wz$P`^0thyzj4M z(NX9HB;UX5#*=)35&H)7vtzKbcpzQy%eNFX1o8imk9=fEM>E(v`+FWkPGS$o^jZ`4 zm(WGus;H<;?7J)+|BI-OB5{+#v2@^@>;J)4+tIb-o?YC|Kx(HM(A$J#HRMFR8?ONlvUHBNli$_Q5?x=rq zfXTv>-n43bSL4^W4Ta*3Sb2MSAns2mSDNwNC^0cHKg-E=n{w5fjimg#_FfP~ReCFkAD>AZ>N$HiKn;8({=STZSSpCr5g7|8RTJ zv#JF0+uYiEp)Bj!=%(Av+0+!qTl}r_?jXG8({0&EA8U@16TCM`x=JG{Ik~S=yB=w9 zaPS=q3yU>^e4Zesxyr8h7_gK!ukh9Lya8If*x|VUZgo%hmFwYNbs4GKv%i(y4u4r$ zkNy0<%O@~Nl}6O0Wno_4i>75tC>s+~7=wHYHdNWZDTCVrrmU{cyV+?nl*q8Xy^Vcz z-A8Po{Oy~>moJ#)e73n|Wf)O?Z=KgNGbvIy3}O{adXC*p8o*`M)%~ViIMHL}6&13e zPLjn;L5Nn9J*AVA6Y?no6O##FhUb1!>Mw8=6bhBP%#184FgQ6dMqIDqw(4EX>5PJW zNRZ|m?G*G{iLUC*pif5@dr!w}KU>_`u65bL8kHn`pzNO{erGEXnx>+{LS=n@(M^GO z92~fErO@G)@B=x@cW{jIFM&4$snb z(mdWF=MRPR39`kch2cWRck*xhh@Gf~2%znDk6uDgR|aVW)Z?oD4|GYzO1PdBc1pNd zBH8iFqvjW-K^93v!GM`ZRZ-dl?`~o-w&IcU2qSIuP&bE{Wl)RpH1t!zyCXK z3KN-=tl{6j-`Uv_ko_s{D3qmDrP1iHCc6E{MJVgjzheizyRRSc|1HkhFobM$zry5b~Z?h z8D42M+GtFge}0n8 zf<&kIEpEpTW*GEo$3^G^8ylsET?05w0sHb4zkGR4Y|*f3d9l;qe-Og1T}NMvg>VrH z8cs<~jT0iJ$v3H#AdlEw_b`z>71A|);N`u6Wfw8^E|d=JkUfF6v|9OYHgOyNRro8f zDwRyvnhqt>?4X`5PfWZy&kVA-l^pvyL$Kp;BQQu#-P+V5F}50QuVz1K)-BEU z7v1yK?7GP_uw4V)ukTya{NbEif2F7ym%2$*qFzMiwmh~q060UO{ox#5+@x59l6h{Y1!-^vd0cw*}6fp;tiuvzFK0MI|u*hz;CcS3z z``56w&@@TZy_JqEZ+7A{wAZS7J!29^#cbsCzEsUTt}vY9AT!`P z$n?PTVaB>)Q#!Mzi) z39tiT!2W2lCuZ#-)SGU!)^8A?%PDMUhaaN@LU&q_Ie-)-K_m0;;bNSHsz%CM(n!~4wlX5(=kii$R(gl`4rUdLCYL9SqIfi| zhaf0G_%&Kby@jmEzx^8zcBKp=4Us#Y7bHs}kXASW0fDlL3YW6^$GCx>8vn;b8hB-X zJ}oPe{9p$V%}&Zu1Pcu|*4FvCxkvy`5@bI?l2cRBT(4d_^rJrMb@URY!|gSFxTlHp zLVC6^W8H@d{4Or1>8iDZR*~TC&J2}ZNYMU%XOEvzpdUcPK$Za_`A2Fh?xKCu^3c!_ z4FGvbLsBv_sNgE1j4_!{pOA}+iarqqOFHcF*)Duvw58g!zT%jnuon`V5xm#Qc)E*$ z(i;60q2^T6)Fk%DaDqI}N>rS*wzSYrv0*XLj#Grs@QdfUORj4|5TWoTsiaBE-mSCX zw$nP_`+vE!C;?HRdriAAa2`%OysL{nXUG&&TQPb}$Dy5&XIcXQTtBw!v?Ou$2&yif~q}|UwG7(iw_9S$vN8YiZ$Cl z=^ap2TYHqj9rm0pl5t{`a;EUZQA1W!UA@-*bTwf9`1sh=)YR}Ji444{u#nzb4+BpR z&;OlKZU_}Fhr26|wjBJKe!Np;Tj^Z4&J+X4h3KTYp`ob2*uvWXj+t`X@y_sE;iHnLdnar1F)%u0huCD~3I8rKy zLI1vs^V5T;F1sdA#XBiAspA0%@GoqtNFMDES0+d2L!y%CZSZoCUm7?(OuIJ-pO{!y zU!MwsfZ=%Oz+j5Nq*&S8$C|1Kho^_fIfY_(j;8{z$R!LfH4UuCf&+=oH~ zOQW@>^Nk`?{9bNeBav)tuuSyV-oSdx$^3Aa1|kl9P{;a2Y(e$oll-&FgLo@kMi0K8 z&o}&fvEPaPtT0l`QPQ=O@m#=&)7H?xHX`&<=hw`Oe%dcsu9+Z@Sg8xJP{ieBo7%S@6Fjq#cc&lLQLnRZL8t4k7iK=N|FJ~2_h&-r!7 z#c9E~S?Q6wdGIe@9Ur$T97VQfK1?09IX@!*S`%l|^Jev~gyUB!(UAeSg+)^GG zWKT$1tUKx{%vpK9<^B-yB?`II9%C1$;=_B#^OFwir5A7OKQOQU@j(bOv-%gtU~3={ zwHJEt*+nDjPxlTK@a4y~Khz623DNV!m50YV{n>DC7J%$xPk7V0T?w1ArAk}W|#0eX6JQ>_B0U$(cs+$ZrMQOEvb<4uwa0r$hPgL#m9@=+uIi^ zUdK=7PX~C((WKPn=`6oh$8D3LZJ{N~+}+*ftRf;&EF~|*6%+FlyRjZw(A*(`j)m2X zVFdG|62yguqaaqAA*L1sI^N$rEG^{Ms_y; zgkWt6$v_|%zvDPL9~PKdoan2~Eu2Idw|sWT`Hh!;CABl0Cep=>nK0!iAu}mv8lJ2) z_YMpIga#-fRkgKAhYmnmup|QFrg|EV)RmX`LOxJIJGImwsyh_E?q`+}HwSx*^%ilS z-Mh!jO@dyRdjgu+=x6IvJNE)6x7HFs>?9{ApC`XO`y8=EyoiL2iVRc>=G}oPpTN0y zcU}jFht!jtF&TM&+|0$ODKLa!Wrxk^W2R#4Y#2oXpAM>!qPQFc?m6S^m zi@ri$$x#1djZX9Q^}iWGvF=OOh4H84{#?ppE~e$)`W83q@@E+ClLSILt?%;mxqsi^ z<+6jAI|-rhn+DC6e>gQDbG#6>fR`hJwYeKi#omQ@7vTJJS#cah*R(47TmdUD%M`T*uwp_s`Vgg*s3j2`hjVF|L z!dMD)>qIDh?%}SN+Ex!*YlEpU;qReP>XJ9!7v3Gs2O&RC%N{!qUwpO0=AN$qnYvfd z=SjEcSS`=jalF_3EYn21Hpu%%DIDE>mS=BzvwQZB>l)9cl^l(d6A-^jZlw-_wTc^(pC&f;`H?73)RFYjGjw-FxipI6_R_K3$$T!?s{PM zg5a)~#9p^NuY>O6p!-=f7;U(r0l4KFm2CuL$!1N!fkpGEl&lqP-FuPM+$%GcgrE=T zEp^mSi!y{%qR92|dvbEi%_y1$`W`4S z-h(ZjBoyMXEM)nIhOG8VpyxgBKhgcY2zvCMdiJDJt(m{Do1ZAP+BH32Q5AvusQ2K~ zgekgPH9g~ad;2G!I@(ya^x`uccYk9jNhH8Z${F6YsHi`2P%cR*Z+*L&v^0`TCj3Fn zGX-t+6IXnoli8OpQEs_uz^f{ZMs48d&RF5?u2B-ll$xE=xfo1eqQghJhF+cZmW z*+X(nd(qlO=-86?SGJcl4bC3NI{2AvINRc-zob)*&E8M1aH_h}Gr*|>E?vBxVtB&i z(uM+wcRAY!`ih9cmADQa*_dy(C?rplBW%Y=Eky+z$ zH*hx2@k$}i!4$}4x}kziv>eFIz3k;4zoET&+0-z9hUhe)w9MN?v=2rFpvVXVy()m) zJkHu9s)OIek)@dt{$Ghz((?Ak#>U}MQFMHKDIZjeKbn2n?hIg)f&wTXChUGP0f&zP z*t)+@dVkQvX>g`mq9|Q5&1p4N$eE)M9wzY|<_nZQKuO)SJXWP+;+-h9&>ayGVRU!7 zUpjlhsi_toHd`o{gj#(_&Tk(KzY7-WPvbNR4VD2l&RFwun~$Qm`e=1eS~rxcG500E z-TaoTQZ`}TV3bVBEAU@gTQC{jsv-%(F>|KgWN>r7V#WghM_CnBZFQ=7iwOBpc&;N39 z)&kGaEoHvP!RZ2|g2g&BdHM=EsTnDk1Z{uIHhqqPSV}A#>MD(qNJ5qgOCEYACfPb6 zNvW@D8WvQ-ZqWs@aiMymf`Vz9`koAA>Ugxw`EN6ij%=;1t^Y7rOj-_(jz-&}m{;wH zFA`;A_>8Yf$eXa`___oaIRl;@N_OUp{vg4B1 zs6>px!#<1sN`{w&g7$qS%?KzqdI))sye>Z3;_H!emY2RlT#Sn$+OJo!Dk^0PHkvvg z+3L;5B0WUfV)#U#AN<5c#mowJaznlz&d2+evu=+^q2tAtzA7myVmPYPuSxCi?=O92 zQEHu@HEHh^ia}y6P=CJW=QeM z`6%eZ^wcX!4N0R8(fb9IbFw~$Yx4qUUyLJn{lEU^BnQ;3VN`w2+qM&)eL=*@>y=uy zfmYMSlS@Kg?(aoJ9$zS8)5kwRi9lH{Zpgif+K%5vYHUhx_uhEh`k7ZP45q-Ta~B(u zaqS*8+aeikG@OqnUNlnvO^k2IuF`2b>*()E!4)0AqHn7ULT;@;^K%PEmL=O)^hF@+BTy&tHTL$%kCd18qDVvsL)XIqXs{CWPmYFVN;(xnb`m$Phi=kI%-NlPw>}&D0 z4@J`DK-zeNu5?oJsszP1&`P|n%c}b3_IB3uzS@Ez(RKr;{^7bJjfkgPq_!A(U~;Iy zV2Ku{mUxG(tLoUvMfWHG(V_F&k~|HVTyN zX880A&Ti0-L`D0b-_ULXK~o#82?RI5Kw@Nge|v3;y!KhbDE;i~OTHYfDd=79w!W$O zc_NoKnID%DG1e`7uS0TX^Z7{;R61d)o+~F4lmnv~32AfjdH|~1m(k5Ov!2$t%Mav> zH+P4&_l{U#e>CE8YDDRdiS34&l9b%c$yTO-6D?U775?e0%+bG9_C&bzkNGc62{{hi z^|*E}$og@_zk6%-!fr;nEfFV*)t&(xe+uNpqjGGh#5{ENq@)97J~@JT=;=$l4!;Rd z?e8uva)}D)pw?m(T2vWs&3!%cH0YUQ<6;p=$j&Sp&nkH@>=cp4a^We`epv??sFC9j@UUe4c4BV43^m3^^qR{0Yx)wUsc zUi_6GE`8P&+805scsW*accz%eI6>#lw}r(?>;}VG%1Vlk-}~c6ZlESqhZmZc`s?$e z{pV$=3_7^KY#wb$xVbqy8dE6IVwGEtlXLscSNrvjw4TZO73gYqPB*jWllE_p=)4eE zM?SiDq)!Ywp8f-1{h`s`nxgXhdK~CS1i>^mp{GMJcyY197_=_z688jsW4ZWRpTj3?6uRK%P^j8j-`diWP_eg|q(Wi1+ib9hZggvf+w}=NTwNwyRzhI@p|tzkIX=21 zX!yURg98JSC7lW>4He;r?nMFW0OCTrkV_K84BYG?+wuSf5j$g?d7OKI5=ghiPag?8 z*Thb~V4QutxPLTSQ^dFzRRqK)IQ-mK@66-i{r{$N{uB! zmB5Ldg$3fLdHeu$@M5*y`#0PkNLoO4HanOpJGVjIOqiLT?itJAZ+1PL3%Z@%e@gOd z*Vooz%+^wxxpoQ;C;0za$qHYnFR&QT9!ux7>A%w92bihP`4H30{Jd;ASW-&57o6!R z3Az7g2gn$LzIQHz#~2tGZD;-D3FV1zzTwxd2TxDw{&oxis240#Flb~4Q=5^Uo&9A4 zXI%p{I_UO-&aqM)9Ib<^0&c^6{8_FlWwog9JOEAd_$e~M`5%i#d-4M~BAVv``q{*) zvb>!g{>gg^_%9!(^{Cf?MSv2h!C#OuPV`<%MPs6#S}`9`s}Hw16RP1<@_^i^iAiqCi=VA-gkZRU@s zg})%7m#XNkpbWO0wIlzeV|C&=CONF*cu|_Dac}qov#6Z+ns=!B?RG}Yp8C4mQfXZa zCtgb19)5m+3}bZdj%5UaGj47?blHgkw(OA`w=heJ zHKSUgIMaF z2YPP1`&MEWeZ~;Ud^||EHTI1qCUaU~Sy%KJIAVvMo(wX?OPqjs-)mg2Qg=atdeKTi zcKm}-)7V}31tAvJYSF}kqU)l>@X~DI&r#b!4#oucMNRnAyQlhyHvpvX?wY@*76rYD z{7=_%831VaxahsDvgKV#T>;Fh$kagH;P9KQsxj*QoSCt*`o?7t4Mg;y(VCgrP5_DQ z!1Cq!E>j}9Kp2IiXvG-nuhiOk75Kbw{kP@nO+%yI+&WQfHt*VnyG;w^pxXG+QG#EB zmhM86&A#7r_)6+ebw8NG<~Qn-Ropt7_N^@)*XeSKA|k%OvdEY6iQaN<4wOW8e0sPI zAhG~li4{;-4401<>lfQRA;IR;GP^bqRK17eV|(!!#rgZCZ_D2~hNS$Sn4?ien7cfu zB!}{nXe5#M5GE4=TMxt%CRF~dOj3t4vGqgLmrKW}$zOyeV=_wZ91HlW3*HS3`m_^S zBHD_A6Nf|pK26dx)2?p%-0q|z=MBMNyTB5r1WmfIJ3AnN0}DQri;F{>eNoh(E&pr? z8c5cP5!ZVXqV#!|`r1=8ik%IdGapF8Q1>P_3&tp`%3g}OrWpT2Y#Zx?#V3V$t#g7n z6v1VsP&sez09vX{6f7dg+jBD_HgGS{rjt;X07Y!T@#D2OAQQgXSCtK()`;lSz_6U; z;7Q2nj6FUrU%a*7na~(mi$&uVO2*9RQ*$V(9+uQ2%$?%)FH3vC2_ZQAr=rDJ1(S;K z(EM66U+;Ng4hVqQT}u9U(*bs7F6EEXuhm_S`{(?i_1)ZpOHE(ut=an?tg29&Q0FpA~{8jbPLBbg2yv={PC#TUgTKY$dzf5GK-7-RYJ}rJsxLy!d2f~+mxe!l zpf-0#vaDe@%3;p&fv|_jd!OQHS0O&@rV8$^>Ug^f&J~4J4s*P#ZwdZWhpKo24-@l8 zt~kfku2S8C2$za@#z%e`@OW-ErjU7@wc;crWPXq!olJLANvtWZEB%=?#R^KHS{M!( zNEU7SX2i z)~&h^_niH(yLWZ1?!CKLt@Yo(UJ+kaWU(>GFc1(Bu;t~X)DaNg_`aUIqoKU^HD4qr zUk}KxpXD{tUYj49dHCy?)JR3`@w)}PtC@?1g@dcLquUwMZwUm1 z4+!#7lA2yw(6vA%O{?y^3&9D5UqZ=$MBPw&LrA-VzY)K6oS)VlvCuH*2bL)(&lgSI z{jzV55b%9(;{gBmyDf9ZJk!)gupgjQ$5H@g;Rj}IaTK>=-fa^B^-L^Qe^P>|aYE~2 zMQO*px&yI-k!Wx(8z!z4B>c zeI@Zi@IC}}eL(w+hWEz^5BVc<_%}|pAUs5=T4H2Lv^O+Mm_Z*x5Z;}nx{0twX-<&xrLzlJwW z1Ttww1MxY7FOhaadmVO@LXP{?Wnv!DW-qxTNm;_TvK(uT+ZnU5ZnIwC#0DUg7BlP) z7B?v5QH{OtwtCv}w`5SFxX!U1`)%LEJnvv><(awlm4t^JtmS_s&AcP<<3$G zD6Q=jd&&hyl5!)^83*0dnxGIJ?ms16J&0DD(EKv$*{ztMOLm`|Ik9J3uF^eTF|S2o z?EvO8%(#BW{On6e@0k0yl7kLlD7ev;-aHX0HnUMG+w~n#Olawr7(8+0qOi_o)@GUC z#)f(0=XsgVkZ|XCtOG{uc0oE@9tA?d-l@+ItHsONc^5>_aRGQTW;bibgo%~(fd=i+b* z@e-Y2w@IIHVqsDCCle4QUN+Ds69P5thlzh$s2CP@Ub~)FKK`(qf8@R+-rqITE@^oA z#!dK`x$NlWhuQ1K{MF{>0Ce|KB1o6id4ivc3B1?H3}XlA@}T;Mn=FU1rH=newFCt% zh0wDd9SsJcHgb0tuS<|W3p6hca`ePk$O=*2R&;7q98{@t_idGW{JjEvoRiVd`zR}l^0=6^Ei7Dh+h7q@8n)szI|PG5 zA$lys0Aa01SO0B>GH!E1wfRHvth$m>I}S4&mOMx$K=-PTPtA&Sk1GOFZ>w-MGcAvw zSkz~r^M_xS^Ty&n4coSe3*AtGs`$q2Hm$HNtGANCZKiKhLD%(@vFIq*uTT>X7J7i; zFS_DdIDD5T^pL3>7b2RSoZg3~$XVkGcZ5CNX+R3n^lOVtk~H}$K(qT#f!H* z*fZVbo`?oJGwdC`WJIO%W>vo4Xx4Qua7~b7jW47NO0zFfRaAdTXT=uNh>axd0u_c#lM>JL#Zn`;_~Ww3Il5%kbU6?2={xlkRU`AmBd_*_UH@6o{M_|bb$apR@7L{~ z_pP|%hM#)a=-o9`7LdDQgQ2LzkdfbKTAq-tA5GM&Pork zuw{p3vw`Ol6y?47twt`t8jJp&9A{3$Jf0>@QFGs;RT#rj=}wWoNc-fQI~f&aBXCyI z*&caWKp+y=De2Ij{)7?nG=ovDlAIVC+Hw_H3Oif1D>k=)mKYhzr5EQgD87|$w_G0? zDKv%D8kaK-Cg>|@X$ecemEIY+Rx_M`IyZ;d;wsA1<7rt)-1d0pam}=DD$HBd9==kRb=@ls#19hTp z)>M}p4@)X}KY40U#x(y!x`>C;q_!VpaR}6L9GBSpnQ#X$!iPeM9uMUEyss3GUM9nU znSa7!$cN;D&Z<4fq2`PXI5F2hUbGmV~yS@ zNepP{F#hoTXLoe8>TI)kHG(1@$1t+RMVhnyvkP<8y76|#@pyp!P&bt;MQj}_zNBow z4`lHv;67NOt)yh|*6e#y=7iq|M|~>hFNp?D0Z!&nf%`SBAv(gL!YFgyR72&t%z$0(e^1bHiPdbtJSj{@Dx)WnZnQ`C*?|!UVE`1l(eB(SW`eKwi*4uIP=Pu9R zA<@YWDh>)#(C!PR)$=DKLCe75dbZmk>gqm0@Eh#*3F0B{ze8`0?|^m*zwZj9n^E{U z^#Sp~clWiwX`!6dSCY=d`afON;xy2-qXrxeojTHNZ!&`|=r? z^QBO-TI3-D0%0AkKS_XJ?5pk+Y8ekLJia-D59aQw$}A>W+`nw>tQ5-bUJ0I}RJsAX z*H&2r;gdZBj-x;o>XeF?CtUczPT02W?kv#ZkvtI&`&D>34e$E+sP|yh?eb9S_w;1E z`y+Mttj3Rly6WNcONVT#MH|rJWvA4y6Xxjh4}7ZsJH8?|fCkaKQpEP#Rb4p zp1RmZ>kGABHVR92Sz9c0yM4b%1p4i4TpHdF+!tP60G^b)0his3an4-Few=|f%WC&` z#DSnQ(H9o@W>TTc{Jr+Quj(gaN8Z(NYr@p}3Fu#`r=Qdc)OF{>`V4#cUOHmsT z$8XgjtuFl;g~S|Ix5~XkoIs8qSU2}*c`{C@`AnF2smMgpP@Mrue6B3SRn&T3A`Slt zs?0n4t#B}h4{;ncwFz8`6`_vOmvO)u`o+noj&V=8c+eE^N0wfhQ!|7o37-VoQR z=ljWY)(0J=vqqz)Dsn_{CTR)(nDi+`7&D@iQW;pi-@aK83l zjXu8AWqQpMwX-{5(7K8C{^8qz(B1QS&)RTt`{*^3lDP#uggyy7XrxD-eW^ocMhwQ$ zz81yJHa~H(H2x2JMax(9YsS84-Wqh5pzDTIeIWq0FCo?rfkNF7Z*gBlFHjk6r3)H} z8Des5kx(e7R^>VKPVGyGhj>(-oE_vC4KYD7&5v%NGyFCSt&`)i{eP1hWgL1cKH5ay z-uFBERwj4v2uS6_*f63RnAlJTn=m3Tqr6ya(7yN*dS5M+eONeTV_bZ~6d6BL*He>e zmyDUyS6P3!&4Ku7f4rCfRa$NfX6pi#`wW#2nSK2G@Np5ZPYUlH4p;Q(Tiny<2n|Lm zL(hvY`5Er@H+md8)>lWvLtYUfOH2e$J%YqXcLE0!t)z&jhSht2cP}`@c=@oHAXs(b z`vrTy3m6p&cZD5h#1BGF-w>>%SQ8*{TfZ-84X7Zug5Y9F=tKwb8B>pusG0z-J*VH+ zmgYK?5JQWmB{2Hb*@2wyCa&~_hxN{9!bpnSIW7oblYyYFi7dtHK$P6f&sHg+Ei94D zoB@qnX7k;=$$KhJjf%Edl2SW$Nh0Xse)i{S30Vakp!z+d$V_%kpnj=ib;)(im(H$0 zO4M@pWJ~rQkwO>?3}j4e7s%`#_A6@sFjO;HueQ@Rzn%1QPw=OBncc31->|@hCUKHr zd!V~yHe-*H+KUU0wx$X9?F`$lL}zan@XNLPZEz9wFe7Vt_z$OXy?SF3W4h80p9y&- z$!Q-}?>)GJENblOZcNx|-f5)s$jCA2ga|yB6xj_L9MJSI)K;Pons2YJ3~W~d(irp|9%`fQ@bVf%^a zj^zu517%y=UWr|MOST8utnXWEZtZ-!e`eMEgt z8XZbN6YGzz>)RKQSXm9_Si5maM1Z{6g~9mEO_BBd{6T;LpFROuV}`VwM?lnQ#cqE{_z%#vHDQdK zTW1OrPjP8xndmkPGX+=? z0q>zm&!$e=E{It=8LF?c|HokLzF$*QzgDNbjA??l;y_UcI2OR0H%jAZNEgV=TF_cR zrs)2*-@A>#IOEJ1VdlzZdi_4SpwpD4yqt-1qAx2jQKAQmW;%y`Pmr@Nm%Vuee%_nN@YxQ~WGwq<5IRDxEi}6EQHavKFccX}pRcnj$0{$W)`WVPDmk4r?3iPyjEe^OlULQ{#2ltQYb z3|g0Dtc;4n%9RK*y$yG6+SCMmI8){p0u{xY1tDR>EmUY?a z>oR8!yN|eO;BP?X`l_QRb@&jRoT{m&lO3nRqoLOgdg%P})N2p!E{%r}M6BN}1+1Z5D-}3_lb9QCHdri=TA|5t@mey2`vC=(xjXF`1 zl#dZN{c5DQuLS1I8|D^@QQ5DW>I38eRA{*Y449kO(i*b2t^y&+!Rv81Psg6hkv3WI zgBUow9$&t7c*BNat3gAb|WSlscTdBZsHPWSc=+?}xW9zybCXpRiXMF#|3sJXoo73m7hlnXAv~7{c z23;@MpqGPziumwp4zq1mR#~go@*U9d9zLjMQNtCxXbCwrJ=NuG1ro^lL!rWgJ6MV? zmRy!*>S#M}A7>@R`5k{6H;!fYg}?lIPgu9BpZB3^qm7Hazfvw?1~Ok%w}n^PPi8T= zSOZvpVwGsYu(X32J!%guLq-%+83o5}nXgAu#LU*B0!%xyvk|ag{@hJ)ndt`KpN8XY z{g)VoOTMfEayw7$(O)T2PR;xA?Y6QJjjOB4`aA2X&zp!tO&9=%Y(C(khOw4{d{b;a z%twY3`K<{KHn!XB+J^GrjYrl4aH-wlRFUJ^k~@YL?dtEel8wo`8(UuKc`ceL%&UQA z_mqL=kEYjl-uM0YnAfjV>YPn|UjsUnduD`&sL0b?&uZ4~yGi@v74aWjrIRQ8O0SSj z7$VaDJNWAVlY0G+m?iuB`(Lf0{$~}bp|3lNNl!-PS0J}cKO^*ibGHAHC;q>vp6~`G z9u*l0$v+r$wbh`S`8P2CYo2foEuFh=qlv)Ik~g`Oi$TqFR|I?^K0VCdt+P#{vI+W5j5IAHTCr?)Vs^*6w=n0 zNUvyFpE;R47~`E69*$N}P;l^Zu|xm`ui=2F#{=xiDgYqx6*Dp+S?iYb4jik z$pHcIhZ=?=XK&Bf$!>Irw0WMb8+bs|8E1qrHU0da(lP@uLW7Emio8$M4$e2+un7nW z`(IbRUnV;V?vEk{;2-?C)rAu)L&UBRmIa{cT{pMz@lB%JznlyXdq8ROb3UrQui1&? zj~F_X*ugRIzDNFPdw30^_GJI~^&gp$jhc^s$8TVjF*(85D#l7mW3;0*G&C}-pAr)j zWoAWHGn^$R&SSQp!1dUZ=B#Cl4Yz=nyj+)KfYBrX{uDAwOGERuOjFX>P<*)dS){26 zyHWSixm8mAn5$}>!OEYfZj+XEdE#RA3;Tg_;IHiAA5hnk*v7n#y12!}Ku8Kx6k}W6 z1H^UmBB?PYj_uZ`>-ySln}i~3WkANXr-~ahMa~v`pi#-C9*9r;l6I@|6KX&zyApk} z-h#c2)hL)`$q-Y>m)KxN+kR0c-Jg#rmLU}lX4<0a;1|V)yT-R4P*O1I497jvit!R) z^qU!9q57PU3%!5;{_x-+?NXrkX8FX&fBXPW2~=@yR_=eG6i%aqE7VzoA(wHh=4OCp zJB`iyobB@jhKKpG%E@7=8pM*O(6IKn`)qenN1{pwj{?E+Ymyk1twJjaBsl(L{ZQ9UY<3Zx2*ob0k@V& z{O+K`1NW^2zBB=Q6FYE%wQ*naZ(}4om+&V3nFY?efU^Jr@x3uYu_ejAi`R+xIJ3?l zs5dM(yYpXc_CcPWAN?R;tHBzNhM5i7MfccFPYmAyqZilA$eP%v*o=F>smHXWduyLj+?(7R-LAK_%cs%>YAudYt_vUMq$h#ya{|pK$DaRW zLMM_XCl-|H;>;&2f5RPdR(Y0k4)cqv2{ZeG)y>Su_n_v3dSz8*h5ajWR_5@|X7`(D z10z*wRaIQYR5owA($@$irqyoWxv3AbacAO&4h4`L$V!z|eQa6?8(+a>Wsu8>Q9VXz z^|>prP7Tv$#c9D$k%&1N&(x(nv-7)qco*P0A`LqxC+XCYt*D}oQxA3?7if0m!T%1= zst+bUM9mA0+pv)wS?Uw5AuW0CP}%z>>PhvOv3T4M4N&s&3X>*RJ~-%EKMUE>*|2Kt z`#ZoVSjA2$6eD_9v&M?P&(S=fXUo}gFVVtcyz<@ElILZo77B3`c9WhwrWciE{H-eC z&@+EEFQ+j1^Zl?V#C~1Mr7~qw0oT=n&UNX!!_#`kerNnS6HF1j*G0xC@?G>X+Jh`E za4hA30_LDBT?O-(GZa*DDXqu0&=b&axM6xEFDn!8HCd!uhZGG4W+1V!D-ngcBZCdL4tW9ji)-t@c;g;=UH`I3kQbb8cw)N^@Nu8`-6FF2_ z)?7!7t$rBBf9(1-(%WvQoObYw@*MWY|#^;ulO|> zLiO2I26YzkUGBt3I^vtHwE-S!v_LTuK?e(9eaNauXnT8{@LWSwmne{HbFJq?Z02ST z3J!I7f7yoIM76o513A09yPx0E@Uf!NztwSZqu*bw63uCoKKp3Ggib8GEvJ zhl@V`uttFV4sY(*^Xh>JE^qqQ=jB)MYXZQj{ub=aZ*#=j*r?}RWgtn=r?Xn=#b>8O ziHj$FnHnTKY!P&(&RMhP_pgJ=ZAF>X&QfP^`g3` zVt=a2&5&T($uW?B?{SeU4!u@@tZ-S7Pkt7LUUzUC$Hb5tzQGf%ZL;+o{ zbbA||-GLJ(B9OYDyC^a5``e-GMf2>+OAHGaH|}XN=1QBqDYxlgvQj1Y+bqA7GJAF} z=%!Y8*H2gA!}QKcVGjU2V17Mgf|FB~Z_p4O&1f$QNbG@t; zavvsSRO8KpECooh6;4`gEFbi0(3b!#n$rp- zZmLG?e3-8~gNoTTCL%(zWY(l*m3S&xtZ!+>-b=Hrh~bheT;4}Xq_kx*%_JORg1oAL$lnVn7Fw8`~BSg*LXAQvtt~Yia_ijs8n-37Cd{1 zVueW!Z{M8;y{#u`Th$1k1yFx3hqmjRPlh0vyoV`N!3e=bA^5Tf~mN|`c{t^J$ zgZqS(yd$Un6{%e*Kum5r2tD4+^n-OYEU9~5jw-8)6eP2t?IXIMTKUUR`9B0upWXRdp)1%>a1JT`sodYhZot zb6p4!;j_iwj!XE4wL)2V_yOV3ofXH8Wc%3)W@W|A!jeN62*vO?zqtWU2_i&JRSyX=gl|Icdk)IAmsYsu=AaM3!+wVYoNq`{=d0eCZ`|l%lqyb9H6;UZUchM!~Z<@LpVOXq*+B?hEOQGN6Ddwz8pvDOL)58}j}TA3Z!yynavcR~^w$&n#M%ri172dmjpU)W9%E4XYM z7ERYz+|& zC-gmx{wfpqOY0CO*L_DGI~tH3)~?;_3ahhnKc6Mks>FZ@=y7Zybmi>aEoUD=9`|Sc zZ^yfQd9RSKlK)njjAqmbsr!a(=TIyh6kc2#fSVU0lbfT)(1X>u29*iQmeUV#=3P3~ zhxcxVrF{P*%wh%9z1IR~h4ex(7%4%&x58xOrbz^MvHl^V!_{hI@M<=t*S1SvIW~l56 zam-YeGh|9;Mo!xYP1D;vDx$u8>jlzLUg!VboS$R13%qZ-eF z5kzWM2P9uudFiY8Je?!Vk}Gv*SRgv-G0wuvHSFP-w6WE*6`H5X=eDf%2StEaaKtbkd;(VMM^P1pL zo(GLj*IXwgZte{M2 z8(oW=nU0uG?kPpTd|{AC8Yk6OY024joAiM_xy&ZVFg-AvQ>(_jK}@YK_7#< zj8$o^R2OIRmCPPb%K(vLl<(fXW7BW8m%%|yQK0XR6h!+EHsBRKeKvN6&FQXi)jO|d z2zxVqFP9>-dUFA~5{FX%36G6665`FKM(5tU`V##JdH=u5oA_e?8^uolJqYs@s9?z0 W!v42T`n7ZnL0(!#s`~S{p#KGecSG?2 literal 0 HcmV?d00001 From fc46588f04b81958b184dd58b955fe7c48319d16 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 18:24:09 -0300 Subject: [PATCH 10/35] only notify when there is a running entry --- run.py | 19 ++++++++++--------- togglore/__init__.py | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/run.py b/run.py index 33ee1c3..0402a29 100644 --- a/run.py +++ b/run.py @@ -38,21 +38,22 @@ expected = 0 actual = 0 + running = 0 if args.command == 'range': - actual, expected = client.diff(utils.DateRange.parse_from_iso_strings(args.from_date, args.to_date)) + actual, expected, running = client.diff(utils.DateRange.parse_from_iso_strings(args.from_date, args.to_date)) elif args.command == 'thisyear': - actual, expected = client.diff(utils.DateRange.this_year(), include_running=True) + actual, expected, running = client.diff(utils.DateRange.this_year(), include_running=True) elif args.command == 'thismonth': - actual, expected = client.diff(utils.DateRange.this_month(), include_running=True) + actual, expected, running = client.diff(utils.DateRange.this_month(), include_running=True) elif args.command == 'thisweek': - actual, expected = client.diff(utils.DateRange.this_week(), include_running=True) + actual, expected, running = client.diff(utils.DateRange.this_week(), include_running=True) elif args.command == 'today': - actual, expected = client.diff(utils.DateRange.today(), include_running=True) + actual, expected, running = client.diff(utils.DateRange.today(), include_running=True) elif args.command == 'month': - actual, expected = client.diff(utils.DateRange.month(int(args.month))) + actual, expected, running = client.diff(utils.DateRange.month(int(args.month))) elif args.command == 'since': - actual, expected = client.diff(utils.DateRange.since(args.since)) + actual, expected, running = client.diff(utils.DateRange.since(args.since)) difference = actual-expected @@ -65,7 +66,7 @@ print(f"Send notification when time is over: {'On' if args.notify else 'Off'}") print(f"Uses notify send when time is over: {'On' if args.uses_notify_send else 'Off'}") - if args.notify and difference >= 0: + if args.notify and difference >= 0 and running: from gi import require_version require_version('Notify', '0.7') from gi.repository import Notify @@ -78,7 +79,7 @@ notification.set_timeout(0) # persist notification.show () - if args.uses_notify_send and difference >= 0: + if args.uses_notify_send and difference >= 0 and running: import os title = f'Time to stop working (+{difference:.2f}h)' os.system( diff --git a/togglore/__init__.py b/togglore/__init__.py index ad1862d..8fd922c 100644 --- a/togglore/__init__.py +++ b/togglore/__init__.py @@ -16,8 +16,8 @@ def __init__(self): def diff(self, date_range, include_running=False): actual_hours = utils.sum_time_of_entries(self.toggle.time_entries(date_range)) expected_hours = self.time_calculator.time_to_work_in_range(date_range) + running_time_entry_hours = utils.get_time_of_running_entry(self.toggle.running_time_entry()) if include_running: - running_time_entry_hours = utils.get_time_of_running_entry(self.toggle.running_time_entry()) actual_hours = actual_hours + running_time_entry_hours - return actual_hours, expected_hours + return actual_hours, expected_hours, running_time_entry_hours From fca5082ea5e7d623996371222563f7667cd7f861 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 18:25:25 -0300 Subject: [PATCH 11/35] add running flag --- run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/run.py b/run.py index 0402a29..bcd8207 100644 --- a/run.py +++ b/run.py @@ -64,6 +64,7 @@ ) print(output_result) + print(f"Running: {'Yes' if running else 'No'}") print(f"Send notification when time is over: {'On' if args.notify else 'Off'}") print(f"Uses notify send when time is over: {'On' if args.uses_notify_send else 'Off'}") if args.notify and difference >= 0 and running: From 2d2e211fa972ee15c801b52b3d20b3289469fd3e Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Fri, 3 Apr 2020 18:29:43 -0300 Subject: [PATCH 12/35] add result image --- README.md | 2 ++ docs/result.jpg | Bin 0 -> 10189 bytes 2 files changed, 2 insertions(+) create mode 100644 docs/result.jpg diff --git a/README.md b/README.md index 6fc2210..f05eca7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Tool for the timetracker [toggle](http://toggl.com/) to calculate the difference between tracked time and the time you should have worked in a given range. +![result](docs/result.jpg) + ## Setup Create a virtual env using python3 and install the requirements. diff --git a/docs/result.jpg b/docs/result.jpg new file mode 100644 index 0000000000000000000000000000000000000000..72155b36f42f050389a47e883e0b5fff0321ad3f GIT binary patch literal 10189 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 Um6^CHDzeSfy$^&nK$y}013RKOWdHyG literal 0 HcmV?d00001 From 58b40d72f5eb289fe4a6effbfddcf241569bb8f1 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 09:35:21 -0300 Subject: [PATCH 13/35] add argument untiltoday --- run.py | 14 ++++++++++++++ togglore/utils.py | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/run.py b/run.py index bcd8207..422598d 100644 --- a/run.py +++ b/run.py @@ -22,6 +22,11 @@ parser_since = subparsers.add_parser('since', help='since help') parser_since.add_argument('since', help='since e.g. 2016.08.01') + parser.add_argument( + '--untiltoday', + action="store_true", + ) + parser.add_argument( '--notify', action="store_true", @@ -43,10 +48,19 @@ if args.command == 'range': actual, expected, running = client.diff(utils.DateRange.parse_from_iso_strings(args.from_date, args.to_date)) elif args.command == 'thisyear': + if args.untiltoday: + actual, expected, running = client.diff(utils.DateRange.this_year_until_today(), include_running=True) + else: actual, expected, running = client.diff(utils.DateRange.this_year(), include_running=True) elif args.command == 'thismonth': + if args.untiltoday: + actual, expected, running = client.diff(utils.DateRange.this_month_until_today(), include_running=True) + else: actual, expected, running = client.diff(utils.DateRange.this_month(), include_running=True) elif args.command == 'thisweek': + if args.untiltoday: + actual, expected, running = client.diff(utils.DateRange.this_week_until_today(), include_running=True) + else: actual, expected, running = client.diff(utils.DateRange.this_week(), include_running=True) elif args.command == 'today': actual, expected, running = client.diff(utils.DateRange.today(), include_running=True) diff --git a/togglore/utils.py b/togglore/utils.py index aa4a913..22ac716 100644 --- a/togglore/utils.py +++ b/togglore/utils.py @@ -70,6 +70,14 @@ def this_week(cls): return cls(start, end) + @classmethod + def this_week_until_today(cls): + today = datetime.date.today() + start = today - datetime.timedelta(today.weekday()) + end = today + + return cls(start, end) + @classmethod def this_month(cls): today = datetime.date.today() @@ -79,6 +87,15 @@ def this_month(cls): return cls(start, end) + @classmethod + def this_month_until_today(cls): + today = datetime.date.today() + __, end_day = calendar.monthrange(today.year, today.month) + start = datetime.date(today.year, today.month, 1) + end = today + + return cls(start, end) + @classmethod def this_year(cls): today = datetime.date.today() @@ -87,6 +104,14 @@ def this_year(cls): return cls(start, end) + @classmethod + def this_year_until_today(cls): + today = datetime.date.today() + start = datetime.date(today.year, 1, 1) + end = today + + return cls(start, end) + @classmethod def month(cls, month): today = datetime.date.today() From d4fd1325aa172431f787625f362b772b8fb1d02f Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 09:35:29 -0300 Subject: [PATCH 14/35] change print --- run.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/run.py b/run.py index 422598d..2b56fc3 100644 --- a/run.py +++ b/run.py @@ -51,17 +51,17 @@ if args.untiltoday: actual, expected, running = client.diff(utils.DateRange.this_year_until_today(), include_running=True) else: - actual, expected, running = client.diff(utils.DateRange.this_year(), include_running=True) + actual, expected, running = client.diff(utils.DateRange.this_year(), include_running=True) elif args.command == 'thismonth': if args.untiltoday: actual, expected, running = client.diff(utils.DateRange.this_month_until_today(), include_running=True) else: - actual, expected, running = client.diff(utils.DateRange.this_month(), include_running=True) + actual, expected, running = client.diff(utils.DateRange.this_month(), include_running=True) elif args.command == 'thisweek': if args.untiltoday: actual, expected, running = client.diff(utils.DateRange.this_week_until_today(), include_running=True) else: - actual, expected, running = client.diff(utils.DateRange.this_week(), include_running=True) + actual, expected, running = client.diff(utils.DateRange.this_week(), include_running=True) elif args.command == 'today': actual, expected, running = client.diff(utils.DateRange.today(), include_running=True) elif args.command == 'month': @@ -76,11 +76,11 @@ ("Hours worked: {0:.2f}h ({1:.2f} days)".format(actual, actual/client.cfg.work_hours_per_day)) + "\r\n" + ("Difference: {0:.2f}h ({1:.2f} days)".format(difference, difference/client.cfg.work_hours_per_day)) ) + print("*"*40) print(output_result) + print("*"*40) - print(f"Running: {'Yes' if running else 'No'}") - print(f"Send notification when time is over: {'On' if args.notify else 'Off'}") - print(f"Uses notify send when time is over: {'On' if args.uses_notify_send else 'Off'}") + print(f"Running time entry: {'Yes' if running else 'No'}") if args.notify and difference >= 0 and running: from gi import require_version require_version('Notify', '0.7') From 4ac96c920e000076812c50d1967772c3d4fd8b0b Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 09:38:16 -0300 Subject: [PATCH 15/35] udpate config template --- README.md | 2 +- config_template.txt | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f05eca7..8b6371b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ API_KEY = 5b9f5e3fd7745a022781daf205f62c72 [Work Hours] hours_per_day = 8.4 -excluded_days = 2016.01.01 +excluded_days = 2020.01.01,2020.01.30 [User Info] id = 123123123 diff --git a/config_template.txt b/config_template.txt index 18d5663..6b1e508 100644 --- a/config_template.txt +++ b/config_template.txt @@ -3,8 +3,9 @@ API_KEY = 5b9f5e3fd7745a022781daf205f62c72 [Work Hours] hours_per_day = 8.4 -excluded_days = 2016.1.1,2016.12.30 +excluded_days = 2020.01.01,2020.01.30 [User Info] -id = 1 -workspace = 1 \ No newline at end of file +id = 123123123 +workspace = 123123123 +project = 123123123 \ No newline at end of file From 3d5cde43bff059c918c8fcecec3b5a6b160dd62c Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 10:15:00 -0300 Subject: [PATCH 16/35] last week and last month --- run.py | 6 ++++++ togglore/utils.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/run.py b/run.py index 2b56fc3..2500bf8 100644 --- a/run.py +++ b/run.py @@ -15,7 +15,9 @@ parser_year = subparsers.add_parser('thisyear', help='today help') parser_thismonth = subparsers.add_parser('thismonth', help='month help') + parser_lastmonth = subparsers.add_parser('lastmonth', help='last month help') parser_week = subparsers.add_parser('thisweek', help='week help') + parser_lastweek = subparsers.add_parser('lastweek', help='this week help') parser_today = subparsers.add_parser('today', help='day help') parser_month = subparsers.add_parser('month', help='month help') parser_month.add_argument('month', help='month e.g. 08') @@ -57,11 +59,15 @@ actual, expected, running = client.diff(utils.DateRange.this_month_until_today(), include_running=True) else: actual, expected, running = client.diff(utils.DateRange.this_month(), include_running=True) + elif args.command == 'lastmonth': + actual, expected, running = client.diff(utils.DateRange.last_month(), include_running=True) elif args.command == 'thisweek': if args.untiltoday: actual, expected, running = client.diff(utils.DateRange.this_week_until_today(), include_running=True) else: actual, expected, running = client.diff(utils.DateRange.this_week(), include_running=True) + elif args.command == 'lastweek': + actual, expected, running = client.diff(utils.DateRange.last_week(), include_running=True) elif args.command == 'today': actual, expected, running = client.diff(utils.DateRange.today(), include_running=True) elif args.command == 'month': diff --git a/togglore/utils.py b/togglore/utils.py index 22ac716..cca872e 100644 --- a/togglore/utils.py +++ b/togglore/utils.py @@ -69,6 +69,14 @@ def this_week(cls): end = start + datetime.timedelta(6) return cls(start, end) + + @classmethod + def last_week(cls): + today = datetime.date.today() + end = today - datetime.timedelta(today.weekday()) - datetime.timedelta(1) + start = end - datetime.timedelta(6) + + return cls(start, end) @classmethod def this_week_until_today(cls): @@ -86,6 +94,20 @@ def this_month(cls): end = datetime.date(today.year, today.month, end_day) return cls(start, end) + + @classmethod + def last_month(cls): + today = datetime.date.today() + month = today.month - 1 + year = today.year + if month == 0: + month = 12 + year = year - 1 + __, end_day = calendar.monthrange(year, month) + start = datetime.date(year, month, 1) + end = datetime.date(year, month, end_day) + + return cls(start, end) @classmethod def this_month_until_today(cls): From 496bd7b38c7dc630bf14f2187f84cdc930d5b01f Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 10:39:01 -0300 Subject: [PATCH 17/35] fix last month calc --- run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run.py b/run.py index 2500bf8..5567e47 100644 --- a/run.py +++ b/run.py @@ -60,14 +60,14 @@ else: actual, expected, running = client.diff(utils.DateRange.this_month(), include_running=True) elif args.command == 'lastmonth': - actual, expected, running = client.diff(utils.DateRange.last_month(), include_running=True) + actual, expected, running = client.diff(utils.DateRange.last_month()) elif args.command == 'thisweek': if args.untiltoday: actual, expected, running = client.diff(utils.DateRange.this_week_until_today(), include_running=True) else: actual, expected, running = client.diff(utils.DateRange.this_week(), include_running=True) elif args.command == 'lastweek': - actual, expected, running = client.diff(utils.DateRange.last_week(), include_running=True) + actual, expected, running = client.diff(utils.DateRange.last_week()) elif args.command == 'today': actual, expected, running = client.diff(utils.DateRange.today(), include_running=True) elif args.command == 'month': From 08770a0275e47d13e66d490cfe9ed92ea16fd9fc Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 10:44:04 -0300 Subject: [PATCH 18/35] add email text generation --- run.py | 19 +++++++++++++++++++ togglore/config.py | 9 +++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/run.py b/run.py index 5567e47..38b97a9 100644 --- a/run.py +++ b/run.py @@ -86,6 +86,25 @@ print(output_result) print("*"*40) + if args.command == 'lastmonth': + email_message = ( + f"Bonjour {client.cfg.boss_name}," + "\n" + + "Je vous envoie le total du mois de ." + "\n" + + "Prévu pour le mois: {0:.2f}h ({1:.2f} jours)".format(expected, expected/client.cfg.work_hours_per_day) + "\n" + + "Total pour le mois: {0:.2f}h ({1:.2f} jours)".format(actual, actual/client.cfg.work_hours_per_day) + "\n" + + "Total: {0:.2f} hrs x {1:.1f} = €{2:.2f}".format(actual, client.cfg.hourly_wage, actual * client.cfg.hourly_wage) + "\n" + + "\n" + + "Quel jour de cette semaine vous pouvez fair le virement?" + "\n" + + "Dans le même jour je vais générer le document fiscale en considerant de la cotation du jour forni par transferwise." + "\n" + + "\n" + + "Je vous souhaite une bonne journée." + ) + print("*"*80) + print("Rapport des heures - ") + print("*"*40) + print(email_message) + print("*"*80) + print(f"Running time entry: {'Yes' if running else 'No'}") if args.notify and difference >= 0 and running: from gi import require_version diff --git a/togglore/config.py b/togglore/config.py index 6095842..a92e91c 100644 --- a/togglore/config.py +++ b/togglore/config.py @@ -3,13 +3,15 @@ class Config(object): - def __init__(self, api_key=None, work_hours_per_day=8.4, excluded_days=[], user_id=1, workspace=1, project=1): + def __init__(self, api_key=None, work_hours_per_day=8.4, excluded_days=[], user_id=1, workspace=1, project=1, boss_name="Boss", hourly_wage=10.0): self.api_key = api_key self.work_hours_per_day = work_hours_per_day self.excluded_days = excluded_days self.user_id = user_id self.workspace = workspace self.project = project + self.boss_name = boss_name + self.hourly_wage = hourly_wage def write_to_file(self, path): cfg = configparser.ConfigParser() @@ -31,6 +33,9 @@ def read_from_file(cls, path): user_id = cfg['User Info']['id'] workspace = cfg['User Info']['workspace'] project = cfg['User Info']['project'] + boss_name = cfg['Personal Details']['boss_name'] + hourly_wage = float(cfg['Personal Details']['hourly_wage']) + day_strings = excluded_days_string.split(',') days = [] @@ -39,4 +44,4 @@ def read_from_file(cls, path): days.append(datetime.datetime.strptime(day_string, "%Y.%m.%d").date()) return cls(api_key=api_key, work_hours_per_day=float(work_hours), excluded_days=days, user_id=user_id, - workspace=workspace, project=project) + workspace=workspace, project=project, boss_name=boss_name, hourly_wage=hourly_wage) From 0a9f567902c546d9490cd18471dd073dc3422681 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 11:12:37 -0300 Subject: [PATCH 19/35] add eur to brl conversion --- run.py | 21 ++++++++++++++++----- togglore/config.py | 6 ++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/run.py b/run.py index 38b97a9..264d607 100644 --- a/run.py +++ b/run.py @@ -77,14 +77,25 @@ difference = actual-expected + + brl = float(client.cfg.eur_to_brl['value']) + brl_update_date = client.cfg.eur_to_brl['date'] + actual_eur = actual * client.cfg.hourly_wage + actual_brl = actual_eur * brl + expected_eur = expected * client.cfg.hourly_wage + expected_brl = expected_eur * brl + difference_eur = difference * client.cfg.hourly_wage + difference_brl = difference_eur * brl + output_result = ( - ("Hours to do: {0:.2f}h ({1:.2f} days)".format(expected, expected/client.cfg.work_hours_per_day)) + "\r\n" + - ("Hours worked: {0:.2f}h ({1:.2f} days)".format(actual, actual/client.cfg.work_hours_per_day)) + "\r\n" + - ("Difference: {0:.2f}h ({1:.2f} days)".format(difference, difference/client.cfg.work_hours_per_day)) + ("Hours to do: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(expected, expected/client.cfg.work_hours_per_day, expected_eur, expected_brl)) + "\r\n" + + ("Hours worked: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(actual, actual/client.cfg.work_hours_per_day, actual_eur, actual_brl)) + "\r\n" + + ("Difference: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(difference, difference/client.cfg.work_hours_per_day, difference_eur, difference_brl)) + "\r\n" + + f"1€ = R${brl} on {brl_update_date}" ) - print("*"*40) + print("*"*60) print(output_result) - print("*"*40) + print("*"*60) if args.command == 'lastmonth': email_message = ( diff --git a/togglore/config.py b/togglore/config.py index a92e91c..8e79fc4 100644 --- a/togglore/config.py +++ b/togglore/config.py @@ -3,7 +3,7 @@ class Config(object): - def __init__(self, api_key=None, work_hours_per_day=8.4, excluded_days=[], user_id=1, workspace=1, project=1, boss_name="Boss", hourly_wage=10.0): + def __init__(self, api_key=None, work_hours_per_day=8.4, excluded_days=[], user_id=1, workspace=1, project=1, boss_name="Boss", hourly_wage=10.0, eur_to_brl={'value': '5.0', 'date': '30/01/2020'}): self.api_key = api_key self.work_hours_per_day = work_hours_per_day self.excluded_days = excluded_days @@ -12,6 +12,7 @@ def __init__(self, api_key=None, work_hours_per_day=8.4, excluded_days=[], user_ self.project = project self.boss_name = boss_name self.hourly_wage = hourly_wage + self.eur_to_brl = eur_to_brl def write_to_file(self, path): cfg = configparser.ConfigParser() @@ -35,6 +36,7 @@ def read_from_file(cls, path): project = cfg['User Info']['project'] boss_name = cfg['Personal Details']['boss_name'] hourly_wage = float(cfg['Personal Details']['hourly_wage']) + eur_to_brl = cfg['EUR to BRL'] day_strings = excluded_days_string.split(',') @@ -44,4 +46,4 @@ def read_from_file(cls, path): days.append(datetime.datetime.strptime(day_string, "%Y.%m.%d").date()) return cls(api_key=api_key, work_hours_per_day=float(work_hours), excluded_days=days, user_id=user_id, - workspace=workspace, project=project, boss_name=boss_name, hourly_wage=hourly_wage) + workspace=workspace, project=project, boss_name=boss_name, hourly_wage=hourly_wage, eur_to_brl=eur_to_brl) From d9b97add2666b761bab6f1b5aff05ed2e7e5996f Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 11:14:07 -0300 Subject: [PATCH 20/35] change conversion text --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index 264d607..13fe69d 100644 --- a/run.py +++ b/run.py @@ -91,7 +91,7 @@ ("Hours to do: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(expected, expected/client.cfg.work_hours_per_day, expected_eur, expected_brl)) + "\r\n" + ("Hours worked: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(actual, actual/client.cfg.work_hours_per_day, actual_eur, actual_brl)) + "\r\n" + ("Difference: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(difference, difference/client.cfg.work_hours_per_day, difference_eur, difference_brl)) + "\r\n" + - f"1€ = R${brl} on {brl_update_date}" + f"1 EUR <-> {brl:.4f} BRL on {brl_update_date}" ) print("*"*60) print(output_result) From 536eb90ddedea5da82969a20f3606bd97e3cc118 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 11:46:11 -0300 Subject: [PATCH 21/35] update cotation command --- run.py | 15 +++++++++++++-- togglore/__init__.py | 4 ++-- togglore/config.py | 18 +++++++++++++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/run.py b/run.py index 13fe69d..b1475c2 100644 --- a/run.py +++ b/run.py @@ -4,7 +4,7 @@ from togglore import utils -if __name__ == '__main__': +def main(): parser = argparse.ArgumentParser(description='Tool for toggle to calculate over/undertime.') subparsers = parser.add_subparsers(dest='command') subparsers.required = True @@ -13,6 +13,8 @@ parser_range.add_argument('from_date', help='startdate, e.g. 30.08.2016') parser_range.add_argument('to_date', help='enddate, e.g. 12.10.2016') + parser_updatecotation = subparsers.add_parser('updatecotation', help='updatecotation help') + parser_year = subparsers.add_parser('thisyear', help='today help') parser_thismonth = subparsers.add_parser('thismonth', help='month help') parser_lastmonth = subparsers.add_parser('lastmonth', help='last month help') @@ -47,6 +49,13 @@ actual = 0 running = 0 + if args.command == 'updatecotation': + client.cfg.update_eur_value(client.config_path) + brl = float(client.cfg.eur_to_brl['value']) + brl_update_date = client.cfg.eur_to_brl['date'] + print(f"* EUR value updated to {brl:.3f} BRL on {brl_update_date}") + return + if args.command == 'range': actual, expected, running = client.diff(utils.DateRange.parse_from_iso_strings(args.from_date, args.to_date)) elif args.command == 'thisyear': @@ -91,7 +100,7 @@ ("Hours to do: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(expected, expected/client.cfg.work_hours_per_day, expected_eur, expected_brl)) + "\r\n" + ("Hours worked: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(actual, actual/client.cfg.work_hours_per_day, actual_eur, actual_brl)) + "\r\n" + ("Difference: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(difference, difference/client.cfg.work_hours_per_day, difference_eur, difference_brl)) + "\r\n" + - f"1 EUR <-> {brl:.4f} BRL on {brl_update_date}" + f"1 EUR <-> {brl:.3f} BRL on {brl_update_date}" ) print("*"*60) print(output_result) @@ -139,3 +148,5 @@ ) +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/togglore/__init__.py b/togglore/__init__.py index 8fd922c..b802ed9 100644 --- a/togglore/__init__.py +++ b/togglore/__init__.py @@ -7,8 +7,8 @@ class Togglore(object): def __init__(self): - config_path = os.path.join(os.path.expanduser('~'), '.togglore') - self.cfg = config.Config.read_from_file(config_path) + self.config_path = os.path.join(os.path.expanduser('~'), '.togglore') + self.cfg = config.Config.read_from_file(self.config_path) self.toggle = toggl.TogglClient(self.cfg.api_key, self.cfg.user_id, self.cfg.workspace, self.cfg.project) self.time_calculator = utils.WorkTimeCalculator(work_hours_per_day=self.cfg.work_hours_per_day, excluded_days=self.cfg.excluded_days) diff --git a/togglore/config.py b/togglore/config.py index 8e79fc4..69b22f8 100644 --- a/togglore/config.py +++ b/togglore/config.py @@ -1,6 +1,6 @@ import configparser import datetime - +import requests class Config(object): def __init__(self, api_key=None, work_hours_per_day=8.4, excluded_days=[], user_id=1, workspace=1, project=1, boss_name="Boss", hourly_wage=10.0, eur_to_brl={'value': '5.0', 'date': '30/01/2020'}): @@ -22,6 +22,22 @@ def write_to_file(self, path): with open(path, 'w') as configfile: cfg.write(configfile) + + def update_eur_value(self, path): + cfg = configparser.ConfigParser() + cfg.read(path) + + url = "https://api.exchangerate-api.com/v6/latest" + response = requests.get(url) + if response.status_code == 200: + result = response.json() + cfg['EUR to BRL']['value'] = str(result['rates']['BRL'] / result['rates']['EUR']) + cfg['EUR to BRL']['date'] = str(datetime.datetime.fromtimestamp(result['time_last_update_unix']).strftime("%d/%m/%Y %H:%M")) + self.eur_to_brl = cfg['EUR to BRL'] + + with open(path, 'w') as configfile: + cfg.write(configfile) + @classmethod def read_from_file(cls, path): From 887a0b774f3b2b3e723ebe84252a36da1e13e23a Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 11:56:39 -0300 Subject: [PATCH 22/35] fix persist --- run.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/run.py b/run.py index b1475c2..cd1e389 100644 --- a/run.py +++ b/run.py @@ -136,7 +136,8 @@ def main(): ('-' * 112) + "\r\n" + output_result, "dialog-information" ) - notification.set_timeout(0) # persist + notification.set_timeout(Notify.EXPIRES_NEVER) # persist + notification.set_urgency(Notify.Urgency.CRITICAL) # persist notification.show () if args.uses_notify_send and difference >= 0 and running: @@ -149,4 +150,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() From 3fabfa4704d6f80cdd0cba6902abc0aa761fa1d2 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 14:19:14 -0300 Subject: [PATCH 23/35] change output format (h and min) --- run.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/run.py b/run.py index cd1e389..b73915c 100644 --- a/run.py +++ b/run.py @@ -97,10 +97,28 @@ def main(): difference_brl = difference_eur * brl output_result = ( - ("Hours to do: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(expected, expected/client.cfg.work_hours_per_day, expected_eur, expected_brl)) + "\r\n" + - ("Hours worked: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(actual, actual/client.cfg.work_hours_per_day, actual_eur, actual_brl)) + "\r\n" + - ("Difference: {0:.2f}h ({1:.2f} days) -> €{2:.2f} - R${3:.2f}".format(difference, difference/client.cfg.work_hours_per_day, difference_eur, difference_brl)) + "\r\n" + - f"1 EUR <-> {brl:.3f} BRL on {brl_update_date}" + "Hours to do: {0:.2f}{1} ({2:.2f} days) -> €{3:.2f} | R${4:.2f}".format( + expected if expected >= 1 else expected * 60, + " h" if expected >= 1 else " min", + expected/client.cfg.work_hours_per_day, + expected_eur, + expected_brl + ) + "\r\n" + + "Hours worked: {0:.2f}{1} ({2:.2f} days) -> €{3:.2f} | R${4:.2f}".format( + actual if actual >= 1 else actual * 60, + " h" if actual >= 1 else " min", + actual/client.cfg.work_hours_per_day, + actual_eur, + actual_brl + ) + "\r\n" + + "Difference: {0:.2f}{1} ({2:.2f} days) -> €{3:.2f} | R${4:.2f}".format( + difference if difference >= 1 else difference * 60, + " h" if difference >= 1 else " min", + difference/client.cfg.work_hours_per_day, + abs(difference_eur), + abs(difference_brl) + ) + "\r\n" + + f"1 EUR <--> {brl:.3f} BRL on {brl_update_date}" ) print("*"*60) print(output_result) From 4ffd2ce5b724a8c953caeba4f35bb2d9cfda9fcd Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 14:37:01 -0300 Subject: [PATCH 24/35] add min to notify menu --- run.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/run.py b/run.py index b73915c..af80640 100644 --- a/run.py +++ b/run.py @@ -150,7 +150,10 @@ def main(): from gi.repository import Notify Notify.init("Toggle Notifier") notification=Notify.Notification.new( - f'Time to stop working (+{difference:.2f}h)', + 'Time to stop working (+{0:.2f}{1})'.format( + difference if difference >= 1 else difference * 60, + " h" if difference >= 1 else " min", + ), ('-' * 112) + "\r\n" + output_result, "dialog-information" ) @@ -160,7 +163,10 @@ def main(): if args.uses_notify_send and difference >= 0 and running: import os - title = f'Time to stop working (+{difference:.2f}h)' + title = 'Time to stop working (+{0:.2f}{1})'.format( + difference if difference >= 1 else difference * 60, + " h" if difference >= 1 else " min", + ) os.system( "notify-send \"" + title + "\" " + " \"" + output_result + "\"" From 074950e6645f6582c34a361fffaa5d8a9d527e77 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 14:50:47 -0300 Subject: [PATCH 25/35] new messages --- run.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/run.py b/run.py index af80640..1809b61 100644 --- a/run.py +++ b/run.py @@ -120,9 +120,6 @@ def main(): ) + "\r\n" + f"1 EUR <--> {brl:.3f} BRL on {brl_update_date}" ) - print("*"*60) - print(output_result) - print("*"*60) if args.command == 'lastmonth': email_message = ( @@ -142,6 +139,50 @@ def main(): print("*"*40) print(email_message) print("*"*80) + elif args.command == 'lastweek': + email_message = ( + f"Bonjour {client.cfg.boss_name}," + "\n" + + "Je vous envoie le total du mois de ." + "\n" + + "Prévu pour le mois: {0:.2f}h ({1:.2f} jours)".format(expected, expected/client.cfg.work_hours_per_day) + "\n" + + "Total pour le mois: {0:.2f}h ({1:.2f} jours)".format(actual, actual/client.cfg.work_hours_per_day) + "\n" + + "Total: {0:.2f} hrs x {1:.1f} = €{2:.2f}".format(actual, client.cfg.hourly_wage, actual * client.cfg.hourly_wage) + "\n" + + "\n" + + "Quel jour de cette semaine vous pouvez fair le virement?" + "\n" + + "Dans le même jour je vais générer le document fiscale en considerant de la cotation du jour forni par transferwise." + "\n" + + "\n" + + "Je vous souhaite une bonne journée." + ) + print("*"*80) + print("Rapport des heures - ") + print("*"*40) + print(email_message) + print("*"*80) + elif args.command == 'thismonth': + expected_end_of_month = client.time_calculator.time_to_work_in_range( + utils.DateRange.this_month() + ) + output_result = output_result + ( + "\r\n" + + "End of the month: {0:.2f} hrs x {1:.1f}€ = €{2:.2f} | R${3:.2f}".format( + expected_end_of_month, + client.cfg.hourly_wage, + expected_end_of_month * client.cfg.hourly_wage, + expected_end_of_month * client.cfg.hourly_wage * brl, + ) + ) + if args.untiltoday: + actual_today, expected_today, running_today = client.diff(utils.DateRange.today(), include_running=True) + output_result = output_result + ( + "\r\n" + + "Today: {0:.2f}{1}".format( + actual_today if actual_today >= 1 else actual_today * 60, + " h" if actual_today >= 1 else " min", + ) + ) + + print("*"*60) + print(output_result) + print("*"*60) print(f"Running time entry: {'Yes' if running else 'No'}") if args.notify and difference >= 0 and running: From 510b9ae0a25ae09e2a9613b70c9ec906030a0499 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 15:23:42 -0300 Subject: [PATCH 26/35] Change weekstart to sunday- --- togglore/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/togglore/utils.py b/togglore/utils.py index cca872e..8925b95 100644 --- a/togglore/utils.py +++ b/togglore/utils.py @@ -65,7 +65,7 @@ def today(cls): @classmethod def this_week(cls): today = datetime.date.today() - start = today - datetime.timedelta(today.weekday()) + start = today - datetime.timedelta(today.weekday() + 1) end = start + datetime.timedelta(6) return cls(start, end) @@ -73,7 +73,7 @@ def this_week(cls): @classmethod def last_week(cls): today = datetime.date.today() - end = today - datetime.timedelta(today.weekday()) - datetime.timedelta(1) + end = today - datetime.timedelta(today.weekday() + 1) - datetime.timedelta(1) start = end - datetime.timedelta(6) return cls(start, end) From dda5c7cc8a0431c8d5dbb85654de5ddcb08e7b28 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Mon, 6 Apr 2020 15:23:57 -0300 Subject: [PATCH 27/35] add lastweek report --- run.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/run.py b/run.py index 1809b61..ba8c636 100644 --- a/run.py +++ b/run.py @@ -140,20 +140,39 @@ def main(): print(email_message) print("*"*80) elif args.command == 'lastweek': + expected_end_of_month = client.time_calculator.time_to_work_in_range( + utils.DateRange.this_month() + ) + date_range = utils.DateRange.last_week() + actual_hours = int(actual) + actual_minutes_float = (actual - actual_hours) * 60 + actual_minutes = int(actual_minutes_float) + actual_seconds = int((actual_minutes_float - actual_minutes) * 60) + # Email template email_message = ( f"Bonjour {client.cfg.boss_name}," + "\n" + - "Je vous envoie le total du mois de ." + "\n" + - "Prévu pour le mois: {0:.2f}h ({1:.2f} jours)".format(expected, expected/client.cfg.work_hours_per_day) + "\n" + - "Total pour le mois: {0:.2f}h ({1:.2f} jours)".format(actual, actual/client.cfg.work_hours_per_day) + "\n" + - "Total: {0:.2f} hrs x {1:.1f} = €{2:.2f}".format(actual, client.cfg.hourly_wage, actual * client.cfg.hourly_wage) + "\n" + - "\n" + - "Quel jour de cette semaine vous pouvez fair le virement?" + "\n" + - "Dans le même jour je vais générer le document fiscale en considerant de la cotation du jour forni par transferwise." + "\n" + - "\n" + - "Je vous souhaite une bonne journée." + "Pour info je vous envoie la quantité des heures que j'ai fait la dernière semaine." + "\n\n" + + "Total (Semaine) : {}:{}:{} ({:.2f} hrs)".format( + actual_hours, + actual_minutes, + actual_seconds, + actual, + ) + "\n" + + "Balance (Semaine) : {}:{} ({:.2f} hrs)".format( + int(difference), + int((difference - int(difference)) * 60), + difference, + ) + "\n" + + "Total prévu pour le mois : {0:.2f}h ({1:.0f} jours)".format( + expected_end_of_month, expected_end_of_month/client.cfg.work_hours_per_day + ) + "\n\n" + + "Cordialement,\nItalo Gustavo Sampaio Fernandes" ) print("*"*80) - print("Rapport des heures - ") + print("Rapport des heures - Semaine {start} à {end}".format( + start=date_range.start.strftime("%d/%m"), + end=date_range.end.strftime("%d/%m") + )) print("*"*40) print(email_message) print("*"*80) From d3a76ebdf7b7e35ab2b2cc214960797ee34e8f5b Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Tue, 7 Apr 2020 09:10:41 -0300 Subject: [PATCH 28/35] fix minute conversion --- run.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/run.py b/run.py index ba8c636..c511ddc 100644 --- a/run.py +++ b/run.py @@ -98,22 +98,22 @@ def main(): output_result = ( "Hours to do: {0:.2f}{1} ({2:.2f} days) -> €{3:.2f} | R${4:.2f}".format( - expected if expected >= 1 else expected * 60, - " h" if expected >= 1 else " min", + expected if abs(expected) >= 1 else expected * 60, + " h" if abs(expected) >= 1 else " min", expected/client.cfg.work_hours_per_day, expected_eur, expected_brl ) + "\r\n" + "Hours worked: {0:.2f}{1} ({2:.2f} days) -> €{3:.2f} | R${4:.2f}".format( - actual if actual >= 1 else actual * 60, - " h" if actual >= 1 else " min", + actual if abs(actual) >= 1 else actual * 60, + " h" if abs(actual) >= 1 else " min", actual/client.cfg.work_hours_per_day, actual_eur, actual_brl ) + "\r\n" + "Difference: {0:.2f}{1} ({2:.2f} days) -> €{3:.2f} | R${4:.2f}".format( - difference if difference >= 1 else difference * 60, - " h" if difference >= 1 else " min", + difference if abs(difference) >= 1 else difference * 60, + " h" if abs(difference) >= 1 else " min", difference/client.cfg.work_hours_per_day, abs(difference_eur), abs(difference_brl) @@ -194,8 +194,8 @@ def main(): output_result = output_result + ( "\r\n" + "Today: {0:.2f}{1}".format( - actual_today if actual_today >= 1 else actual_today * 60, - " h" if actual_today >= 1 else " min", + actual_today if abs(actual_today) >= 1 else actual_today * 60, + " h" if abs(actual_today) >= 1 else " min", ) ) @@ -211,8 +211,8 @@ def main(): Notify.init("Toggle Notifier") notification=Notify.Notification.new( 'Time to stop working (+{0:.2f}{1})'.format( - difference if difference >= 1 else difference * 60, - " h" if difference >= 1 else " min", + difference if abs(difference) >= 1 else difference * 60, + " h" if abs(difference) >= 1 else " min", ), ('-' * 112) + "\r\n" + output_result, "dialog-information" @@ -224,8 +224,8 @@ def main(): if args.uses_notify_send and difference >= 0 and running: import os title = 'Time to stop working (+{0:.2f}{1})'.format( - difference if difference >= 1 else difference * 60, - " h" if difference >= 1 else " min", + difference if abs(difference) >= 1 else difference * 60, + " h" if abs(difference) >= 1 else " min", ) os.system( "notify-send \"" + title + "\" " + " \"" + From decb8af50444b1164005e71203a122228dc88001 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Tue, 7 Apr 2020 09:13:44 -0300 Subject: [PATCH 29/35] update today --- run.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/run.py b/run.py index c511ddc..5ecb839 100644 --- a/run.py +++ b/run.py @@ -198,6 +198,20 @@ def main(): " h" if abs(actual_today) >= 1 else " min", ) ) + elif args.command == 'today': + expected_end_of_month = client.time_calculator.time_to_work_in_range( + utils.DateRange.this_month() + ) + output_result = output_result + ( + "\r\n" + + "End of the month: {0:.2f} hrs x {1:.1f}€ = €{2:.2f} | R${3:.2f}".format( + expected_end_of_month, + client.cfg.hourly_wage, + expected_end_of_month * client.cfg.hourly_wage, + expected_end_of_month * client.cfg.hourly_wage * brl, + ) + ) + print("*"*60) print(output_result) From 3be4e18191fe0e258735871249b344f827de7cbb Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Tue, 7 Apr 2020 10:25:51 -0300 Subject: [PATCH 30/35] change layout --- run.py | 53 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/run.py b/run.py index 5ecb839..5ba8b1d 100644 --- a/run.py +++ b/run.py @@ -1,4 +1,5 @@ import argparse +import datetime import togglore from togglore import utils @@ -97,28 +98,29 @@ def main(): difference_brl = difference_eur * brl output_result = ( - "Hours to do: {0:.2f}{1} ({2:.2f} days) -> €{3:.2f} | R${4:.2f}".format( + "Hours to do:\t{0:5.2f}{1} ({2:5.2f} days) -> €{3:6.2f} | R$ {4:4.0f}".format( expected if abs(expected) >= 1 else expected * 60, " h" if abs(expected) >= 1 else " min", expected/client.cfg.work_hours_per_day, expected_eur, expected_brl ) + "\r\n" + - "Hours worked: {0:.2f}{1} ({2:.2f} days) -> €{3:.2f} | R${4:.2f}".format( + "Hours worked:\t{0:5.2f}{1} ({2:5.2f} days) -> €{3:6.2f} | R$ {4:4.0f}".format( actual if abs(actual) >= 1 else actual * 60, " h" if abs(actual) >= 1 else " min", actual/client.cfg.work_hours_per_day, actual_eur, actual_brl ) + "\r\n" + - "Difference: {0:.2f}{1} ({2:.2f} days) -> €{3:.2f} | R${4:.2f}".format( + "Difference:\t{0:5.2f}{1} ({2:.2f} days) -> €{3:6.2f} | R$ {4:4.0f}".format( difference if abs(difference) >= 1 else difference * 60, " h" if abs(difference) >= 1 else " min", difference/client.cfg.work_hours_per_day, abs(difference_eur), abs(difference_brl) ) + "\r\n" + - f"1 EUR <--> {brl:.3f} BRL on {brl_update_date}" + "-" * 60 + "\r\n" + + f"Cotation on {brl_update_date}: 1 EUR = {brl:.3f} BRL" ) if args.command == 'lastmonth': @@ -182,9 +184,10 @@ def main(): ) output_result = output_result + ( "\r\n" + - "End of the month: {0:.2f} hrs x {1:.1f}€ = €{2:.2f} | R${3:.2f}".format( + "Total: {0:.2f} hrs x {1:.1f}€ (R$ {2:.1f}) = €{3:.2f} | R$ {4:.0f}".format( expected_end_of_month, client.cfg.hourly_wage, + client.cfg.hourly_wage * brl, expected_end_of_month * client.cfg.hourly_wage, expected_end_of_month * client.cfg.hourly_wage * brl, ) @@ -193,10 +196,22 @@ def main(): actual_today, expected_today, running_today = client.diff(utils.DateRange.today(), include_running=True) output_result = output_result + ( "\r\n" + - "Today: {0:.2f}{1}".format( - actual_today if abs(actual_today) >= 1 else actual_today * 60, - " h" if abs(actual_today) >= 1 else " min", + "-" * 60 + "\r\n" + ) + if difference <= 0: + finish_prevision = ( + datetime.datetime.now() + + datetime.timedelta(hours=-difference) + ).strftime("%H:%M") + output_result = output_result + ( + f"Finish prevision ({-difference:.2f}h): {finish_prevision}" + "\r\n" + + "-" * 60 + "\r\n" ) + output_result = output_result + ( + "Today: " + f"{actual_today:.2f}h / {expected-actual+actual_today:.2f}h" + "\r\n" + + "{0:3.0f}%".format(100*actual/expected) + " " + "[" + "=" * int(actual*54/expected) + "-" * int(-(difference)*54/expected) + "]" + "\r\n" + + "This Month: " + f"{actual/client.cfg.work_hours_per_day:.2f} / {(expected_end_of_month/client.cfg.work_hours_per_day):.0f} days - " + f"R$ {actual_brl:.0f} / R$ {(expected_end_of_month * client.cfg.hourly_wage * brl):.0f}" + "\r\n" + + "{0:3.0f}%".format(100*actual/expected_end_of_month) + " " + "[" + "=" * int(actual*54/expected_end_of_month) + "-" * int(-(actual-expected_end_of_month)*54/expected_end_of_month) + "]" ) elif args.command == 'today': expected_end_of_month = client.time_calculator.time_to_work_in_range( @@ -204,20 +219,38 @@ def main(): ) output_result = output_result + ( "\r\n" + - "End of the month: {0:.2f} hrs x {1:.1f}€ = €{2:.2f} | R${3:.2f}".format( + "Total: {0:.2f} hrs x {1:.1f}€ (R$ {2:.1f}) = €{3:.2f} | R$ {4:.0f}".format( expected_end_of_month, client.cfg.hourly_wage, + client.cfg.hourly_wage * brl, expected_end_of_month * client.cfg.hourly_wage, expected_end_of_month * client.cfg.hourly_wage * brl, ) ) + output_result = output_result + ( + "\r\n" + + "-" * 60 + "\r\n" + ) + if difference <= 0: + finish_prevision = ( + datetime.datetime.now() + + datetime.timedelta(hours=-difference) + ).strftime("%H:%M") + output_result = output_result + ( + f"Finish prevision ({-difference:.2f}h): {finish_prevision}" + "\r\n" + + "-" * 60 + "\r\n" + ) + output_result=output_result + ( + "Today: " + f"{actual:.2f}h / {expected:.2f}h" + "\r\n" + + "{0:3.0f}%".format(100*actual/expected) + " " + "[" + "=" * int(actual*54/expected) + "-" * int(-difference*54/expected) + "]" + ) print("*"*60) print(output_result) print("*"*60) - print(f"Running time entry: {'Yes' if running else 'No'}") + print(f"Working now: {'Yes' if running else 'No'}") if args.notify and difference >= 0 and running: from gi import require_version require_version('Notify', '0.7') From 88f09400b36a3c581fd0442c03f33a95ee845e9c Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Tue, 7 Apr 2020 10:27:14 -0300 Subject: [PATCH 31/35] fix layout --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index 5ba8b1d..da572de 100644 --- a/run.py +++ b/run.py @@ -112,7 +112,7 @@ def main(): actual_eur, actual_brl ) + "\r\n" + - "Difference:\t{0:5.2f}{1} ({2:.2f} days) -> €{3:6.2f} | R$ {4:4.0f}".format( + "Difference:\t{0:5.2f}{1} ({2:5.2f} days) -> €{3:6.2f} | R$ {4:4.0f}".format( difference if abs(difference) >= 1 else difference * 60, " h" if abs(difference) >= 1 else " min", difference/client.cfg.work_hours_per_day, From 886bd5048f533a9da77b4504c865cb2feff942bc Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Tue, 7 Apr 2020 10:27:38 -0300 Subject: [PATCH 32/35] fix typo --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index da572de..25fa9c4 100644 --- a/run.py +++ b/run.py @@ -153,7 +153,7 @@ def main(): # Email template email_message = ( f"Bonjour {client.cfg.boss_name}," + "\n" + - "Pour info je vous envoie la quantité des heures que j'ai fait la dernière semaine." + "\n\n" + + "Pour info je vous envoie la quantité des heures que j'ai fait la semaine dernière." + "\n\n" + "Total (Semaine) : {}:{}:{} ({:.2f} hrs)".format( actual_hours, actual_minutes, From fb40aadf041dca4e5c60b8e3c6c255e525b54143 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Tue, 7 Apr 2020 10:31:03 -0300 Subject: [PATCH 33/35] change text --- run.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/run.py b/run.py index 25fa9c4..0c75d19 100644 --- a/run.py +++ b/run.py @@ -98,21 +98,21 @@ def main(): difference_brl = difference_eur * brl output_result = ( - "Hours to do:\t{0:5.2f}{1} ({2:5.2f} days) -> €{3:6.2f} | R$ {4:4.0f}".format( + "Hours to do:\t{0:6.2f}{1} ({2:5.2f} days) -> €{3:7.2f} | R$ {4:4.0f}".format( expected if abs(expected) >= 1 else expected * 60, " h" if abs(expected) >= 1 else " min", expected/client.cfg.work_hours_per_day, expected_eur, expected_brl ) + "\r\n" + - "Hours worked:\t{0:5.2f}{1} ({2:5.2f} days) -> €{3:6.2f} | R$ {4:4.0f}".format( + "Hours worked:\t{0:6.2f}{1} ({2:5.2f} days) -> €{3:7.2f} | R$ {4:4.0f}".format( actual if abs(actual) >= 1 else actual * 60, " h" if abs(actual) >= 1 else " min", actual/client.cfg.work_hours_per_day, actual_eur, actual_brl ) + "\r\n" + - "Difference:\t{0:5.2f}{1} ({2:5.2f} days) -> €{3:6.2f} | R$ {4:4.0f}".format( + "Difference:\t{0:6.2f}{1} ({2:5.2f} days) -> €{3:7.2f} | R$ {4:4.0f}".format( difference if abs(difference) >= 1 else difference * 60, " h" if abs(difference) >= 1 else " min", difference/client.cfg.work_hours_per_day, @@ -138,7 +138,7 @@ def main(): ) print("*"*80) print("Rapport des heures - ") - print("*"*40) + print("-"*45) print(email_message) print("*"*80) elif args.command == 'lastweek': @@ -175,7 +175,7 @@ def main(): start=date_range.start.strftime("%d/%m"), end=date_range.end.strftime("%d/%m") )) - print("*"*40) + print("-"*45) print(email_message) print("*"*80) elif args.command == 'thismonth': From 8888048f6cfc461609e0d4183c768c239a972701 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Tue, 7 Apr 2020 10:32:24 -0300 Subject: [PATCH 34/35] fix calc --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index 0c75d19..cc22d94 100644 --- a/run.py +++ b/run.py @@ -209,7 +209,7 @@ def main(): ) output_result = output_result + ( "Today: " + f"{actual_today:.2f}h / {expected-actual+actual_today:.2f}h" + "\r\n" + - "{0:3.0f}%".format(100*actual/expected) + " " + "[" + "=" * int(actual*54/expected) + "-" * int(-(difference)*54/expected) + "]" + "\r\n" + + "{0:3.0f}%".format(100*actual_today/(expected-actual+actual_today)) + " " + "[" + "=" * int(actual_today*54/(expected-actual+actual_today)) + "-" * int(-(difference)*54/(expected-actual+actual_today)) + "]" + "\r\n" + "This Month: " + f"{actual/client.cfg.work_hours_per_day:.2f} / {(expected_end_of_month/client.cfg.work_hours_per_day):.0f} days - " + f"R$ {actual_brl:.0f} / R$ {(expected_end_of_month * client.cfg.hourly_wage * brl):.0f}" + "\r\n" + "{0:3.0f}%".format(100*actual/expected_end_of_month) + " " + "[" + "=" * int(actual*54/expected_end_of_month) + "-" * int(-(actual-expected_end_of_month)*54/expected_end_of_month) + "]" ) From 868bd5fa20f605b208c6788de4e6b89238a87298 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Wed, 8 Apr 2020 08:34:58 -0300 Subject: [PATCH 35/35] change min text to m --- run.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/run.py b/run.py index cc22d94..b3d0e8d 100644 --- a/run.py +++ b/run.py @@ -100,21 +100,21 @@ def main(): output_result = ( "Hours to do:\t{0:6.2f}{1} ({2:5.2f} days) -> €{3:7.2f} | R$ {4:4.0f}".format( expected if abs(expected) >= 1 else expected * 60, - " h" if abs(expected) >= 1 else " min", + " h" if abs(expected) >= 1 else " m", expected/client.cfg.work_hours_per_day, expected_eur, expected_brl ) + "\r\n" + "Hours worked:\t{0:6.2f}{1} ({2:5.2f} days) -> €{3:7.2f} | R$ {4:4.0f}".format( actual if abs(actual) >= 1 else actual * 60, - " h" if abs(actual) >= 1 else " min", + " h" if abs(actual) >= 1 else " m", actual/client.cfg.work_hours_per_day, actual_eur, actual_brl ) + "\r\n" + "Difference:\t{0:6.2f}{1} ({2:5.2f} days) -> €{3:7.2f} | R$ {4:4.0f}".format( difference if abs(difference) >= 1 else difference * 60, - " h" if abs(difference) >= 1 else " min", + " h" if abs(difference) >= 1 else " m", difference/client.cfg.work_hours_per_day, abs(difference_eur), abs(difference_brl) @@ -259,7 +259,7 @@ def main(): notification=Notify.Notification.new( 'Time to stop working (+{0:.2f}{1})'.format( difference if abs(difference) >= 1 else difference * 60, - " h" if abs(difference) >= 1 else " min", + " h" if abs(difference) >= 1 else " m", ), ('-' * 112) + "\r\n" + output_result, "dialog-information" @@ -272,7 +272,7 @@ def main(): import os title = 'Time to stop working (+{0:.2f}{1})'.format( difference if abs(difference) >= 1 else difference * 60, - " h" if abs(difference) >= 1 else " min", + " h" if abs(difference) >= 1 else " m", ) os.system( "notify-send \"" + title + "\" " + " \"" +