#!/usr/bin/env python3 ######################### # # timeTrack.py # by 4nima # v.2.1.2 # ######################### # simple time tracking with database ######################### import datetime import sqlite3 import json import os import logging # Main class class TimeTrack: def __init__(self, DATABASE='timetrack.db', CONFIG='timetrack.conf'): self.DATABASE = DATABASE self.CONFIG = CONFIG self.USERID = 0 self.USERNAME = '' self.OLDEVENT = 2 self.LOGFILE = 'timetrack.log' self.DBCON = sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) logging.basicConfig( filename=self.LOGFILE, level=logging.DEBUG, format='%(asctime)s - %(process)d-%(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) self.db_setup() self.load_config() def __del__(self): self.DBCON.close() ## Prepartation #============== ### Check OS and clear screen def clear_screen(self): if os.name == 'posix': logging.debug('Unix/Linux system detected') _ = os.system('clear') else: logging.debug('Winwos System detected') _ = os.system('cls') ### Loads or creates a config file def load_config(self): if os.path.isfile(self.CONFIG): logging.info('Config file was found') try: with open(self.CONFIG) as config_data: data = json.load(config_data) except ValueError: logging.error('Config file has no valid JSON syntax') print('TimeTrack wird beendet: Fehler in der JSON-Konfig ({})'.format(self.CONFIG)) quit() else: logging.info('Config file was loaded successfully') self.USERID = data['user'] self.OLDEVENT = data['oldevent'] logging.debug('UserID {} was used'.format(data['user'])) self.set_user() else: logging.warning('Config file not found') config = { 'default' : 'interactive', 'user' : 1, 'oldevent' : 2 } with open(self.CONFIG, "w") as outfile: json.dump(config, outfile, indent=4, sort_keys=True) logging.info('Config file created successfully') #> wenn man keine datei erstellen darf/kann, fällt das skript in einen loop ? self.load_config() ### Creates a database if none is found def db_setup(self): if os.path.isfile(self.DATABASE): logging.info('Database file was found') else: logging.info('Database file was not found, will create ') sql = [] sql.append(""" CREATE TABLE IF NOT EXISTS time_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, starttime TIMESTAMP NOT NULL, endtime TIMESTAMP NOT NULL, breaktime1 TIMESTAMP, user_id INTEGER NOT NULL, activity TEXT, reference TEXT, category_id INTEGER, client_id INTEGER, lock BOOLEAN ) """) sql.append(""" CREATE TABLE IF NOT EXISTS active_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, starttime TIMESTAMP NOT NULL, breaktime TIMESTAMP, user_id INT NOT NULL ) """) sql.append(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, worktime INT, worktime_span INT ) """) sql.append(""" CREATE TABLE IF NOT EXISTS clients ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL ) """) sql.append(""" CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL ) """) logging.debug('Create initial database tables') try: with self.DBCON as con: for SQL in sql: con.execute(SQL) except sqlite3.Error as err: logging.error('Table could not be created') logging.debug(sql) logging.error('SQLError: {}'.format(err)) print('TimeTrack wird beendet: Fehler bei Datanbank Setup') quit() else: logging.debug('Table was created successfully or already exists') ### Get an active event based on a user or event ID def get_active_event(self, USERID='', EVENTID=''): if USERID: logging.debug('Search events based on userid: {}'.format(USERID)) sql = "SELECT * FROM active_events WHERE user_id = ?" searchdata = [USERID] elif EVENTID: logging.debug('Search event based on eventid: {}'.format(EVENTID)) sql = "SELECT * FROM active_events WHERE id = ?" searchdata = [EVENTID] else: sql = "SELECT * FROM active_events" searchdata = False try: with sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) as con: cur = con.cursor() if searchdata: logging.debug('Search event') cur.execute(sql, searchdata) else: logging.debug('Get all Events') cur.execute(sql) eventdata = cur.fetchall() except sqlite3.Error as err: logging.debug(sql) logging.error('SQLError: {}'.format(err)) print('Fehler beim auslesen der aktiven Events') else: logging.debug('Events could be read out successfully') con.close() if eventdata == []: logging.debug('No active events found') return 0 else: logging.debug('{} events found'.format(len(eventdata))) return eventdata[0] ### Get time entries based on various criteria def get_time_entry(self, USERID='', TIMEID='', CATEGORYID='', CLIENTID='', REFERENCE='', STARTTIME='', ENDTIME='', DAY=''): if USERID: logging.debug('Search time entries based on userid: {}'.format(USERID)) sql = "SELECT * FROM time_entries WHERE user_id = ?" searchdata = [USERID] elif TIMEID: logging.debug('Search time entrys based on timeid: {}'.format(TIMEID)) sql = "SELECT * FROM time_entries WHERE id = ?" searchdata = [TIMEID] elif CATEGORYID: logging.debug('Search time entrys based on categoryid: {}'.format(CATEGORYID)) sql = "SELECT * FROM time_entries WHERE category_id = ?" searchdata = [CATEGORYID] elif CLIENTID: logging.debug('Search time entrys based on clientid: {}'.format(CLIENTID)) sql = "SELECT * FROM time_entries WHERE client_id = ?" searchdata = [CLIENTID] elif REFERENCE: logging.debug('Search time entrys based on reference: {}'.format(REFERENCE)) sql = "SELECT * FROM time_entries WHERE reference = ?" searchdata = [REFERENCE] else: sql = "SELECT * FROM time_entries" searchdata = False try: with sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) as con: cur = con.cursor() if searchdata: logging.debug('Search time entry') cur.execute(sql, searchdata) else: logging.debug('Get all time entries') cur.execute(sql) timedata = cur.fetchall() except sqlite3.Error as err: logging.debug(sql) logging.error('SQLError: {}'.format(err)) print('Fehler beim auslesen der Zeiteinträge') else: logging.debug('Time entries could be read out successfully') con.close() if DAY: logging.debug('Search time entries by date: {}'.format(DAY)) tmp = [] for entry in timedata: if entry[1].date() == DAY: tmp.append(entry) timedata = tmp[:] else: tmp = [] if STARTTIME: logging.debug('Search time entries by starttime: {}'.format(STARTTIME)) for entry in timedata: if entry[1] > STARTTIME: tmp.append(entry) timedata = tmp[:] pass tmp = [] if ENDTIME: logging.debug('Search time entrie by endtime: {}'.format(ENDTIME)) for entry in timedata: if entry[2] < ENDTIME: tmp.append(entry) timedata = tmp[:] if timedata == []: logging.debug('No time entries found') return 0 else: logging.debug('{} time entries found'.format(len(timedata))) return timedata ## user handling #=============== ### Creates a user who does not yet exist def create_user(self, USER=''): if USER == '': username = input('Benutzername eingaben: ') logging.debug('Selected username: {}'.format(username)) else: username = USER while self.get_users(NAME=username): print('Der gewünsche Benutzername ({}) ist schon vergeben'.format(username)) username = input('Wähle einen anderen Benutzernamen: ') logging.debug('Try again: Selected username: {}'.format(username)) logging.debug('Accepted username: {}'.format(username)) sql = "INSERT INTO users ( name ) values ( ? )" try: with self.DBCON as con: con.execute(sql, [username]) except sqlite3.Error as err: logging.error('User could not be saved in database') logging.debug(sql) logging.error('SQLError: {}'.format(err)) else: logging.info('User was saved successfully') ### Outputs existing users from the DB def get_users(self, UID=0, NAME=''): if not UID == 0: logging.debug('Get user by ID: {}'.format(UID)) data = UID sql = "SELECT * FROM users WHERE id = ?" elif not NAME == '': logging.debug('Get user by username: {}'.format(NAME)) data = NAME sql = "SELECT * FROM users WHERE name LIKE ?" else: logging.debug('Get all users') data = '' sql = "SELECT * FROM users" try: with self.DBCON as con: cur = con.cursor() if data: cur.execute(sql, [data]) else: cur.execute(sql) data = cur.fetchall() except sqlite3.Error as err: logging.error('Could not get user') logging.debug(sql) logging.error('SQLError: {}'.format(err)) print('Fehler beim Zugriff auf die Benutzer Datenbank') return 1 else: logging.debug('User database read out successfully') return data ### Defines a user for the session def set_user(self): data = self.get_users() if not data: logging.info("No user was found") print("Es wurde kein Benutzer gefunden, bitte legen sie einen neuen an.") self.create_user() data = self.get_users(UID=self.USERID) if data == []: logging.error('User ID was not found') else: self.USERNAME = data[0][1] ## Time handling #=============== ### Creates an active event if none exists for the user def save_event(self, TIME=datetime.datetime.now()): if not self.get_active_event(USERID=self.USERID): logging.debug('No active events found for the user: {}'.format(self.USERID)) sql = "INSERT INTO active_events ( starttime, user_id ) VALUES ( ?, ? )" try: with self.DBCON as con: con.execute(sql, [TIME, self.USERID]) except sqlite3.Error as err: logging.error('Event could not be created') logging.debug(sql) logging.error('SQLError: {}'.format(err)) print('Event konnte nicht gespeichert werden.') return False else: logging.info('Event was created successfully') return True else: logging.warning('Active events found for the user, new event could not be created') return False ### Deletes an active event based on a user or event ID def delete_event(self, ENTRYID='', USERID=''): if not ENTRYID == '': logging.info('Deletes event based on eventid: {}'.format(ENTRYID)) sql = "DELETE FROM active_events WHERE id = ?" data = ENTRYID elif not USERID == '': logging.info('Deletes events based on userid: {}'.format(USERID)) sql = "DELETE FROM active_events WHERE user_id = ?" data = USERID else: logging.warning('No indication of what should be deleted') print('Keine angabe was gelöscht werden soll') return 0 try: with self.DBCON as con: con.execute(sql, [data]) except sqlite3.Error as err: logging.error('Event could not be deleted') logging.debug(sql) logging.error('SQLError: {}'.format(err)) print('Fehler beim löschen des Events.') else: logging.debug('Event was successfully deleted') ### Checks for existing time entries and creates a new one if none is available. def time_start(self, AUTOFORWARD=True): self.clear_screen() starttime = datetime.datetime.now() logging.debug('New Event process started at {}'.format(starttime)) if self.save_event(starttime): print('Neues Event gestartet um {}'.format(starttime.strftime("%H:%M"))) if AUTOFORWARD: self.time_stop() else: data = self.get_active_event(USERID=self.USERID) print('Es existiert bereits ein aktives Event.') if (data[1] + datetime.timedelta(hours=self.OLDEVENT)) <= datetime.datetime.now(): logging.info('Event exceeds allowed duration') print('Zeiteintrag ist zu alt laut den Einstellungen (älter {} Stunden)'.format(self.OLDEVENT)) if datetime.date.today() == data[1].date(): print('Start um: {}'.format(data[1].strftime("%H:%M"))) else: print('Start am: {}'.format(data[1].strftime("%d.%m.%Y um %H:%M"))) elapsed = datetime.datetime.now() - data[1] if elapsed.days: logging.debug('Event older than 1 day ({} days)'.format(elapsed.days)) print('Vergangene Zeit: >{} Tage'.format(elapsed.days)) else: logging.debug('Event younger than 1 day') print('Vergangene Zeit: >{} Stunden'.format(int(elapsed.seconds/3600))) printtext = [ 'Wie soll mit dem Event verfahren werden?', '[1] fortsetzen', '[2] löschen', '[0] abbrechen' ] userinput = self.userchoise(printtext, 3) self.clear_screen() if userinput == 1: logging.debug('Event should be continued') self.time_stop() elif userinput == 2: logging.info('Event should be deleted (eventid: {})'.format(data[0])) self.delete_event(data[0]) self.time_start() else: logging.debug('Event continues (eventid{})'.format(data[0])) print('Event von {} Uhr geladen'.format(data[1].strftime("%H:%M"))) self.time_stop() ### Stops existing time entries def time_stop(self): data = self.get_active_event(USERID=self.USERID) logging.debug('Event stop progess is started') if data: self.clear_screen() printtext = [ 'Event von {} Uhr beenden?'.format(data[1].strftime("%H:%M")), '[1] für beenden', '[2] für löschen', '[0] für abbrechen' ] userinput = self.userchoise(printtext, 3) self.clear_screen() if userinput == 1: logging.debug('Event is ended') print('Eingabe beenden mittels doppelter Leerzeile.') print('Durchgeführte Tätigkeiten:') userinput = [] while True: try: if userinput[-1] == '' and userinput[-2] == '': break except IndexError: pass userinput.append(input()) logging.debug('User input: {}'.format(userinput)) del userinput[-1] del userinput[-1] action = '' for i in userinput: if not action == '': action += "\n" action += i endtime = datetime.datetime.now() logging.debug('Event end process start at {}'.format(endtime)) sql = "INSERT INTO time_entries ( starttime, endtime, user_id, activity ) VALUES ( ?, ?, ?, ? )" try: with self.DBCON as con: con.execute(sql, [data[1], endtime, self.USERID, action]) except sqlite3.Error as err: logging.error('Time entry could not be created') logging.debug(sql) logging.error('SQLError: {}'.format(err)) print('Zeiteintrag konnte nicht gespeichert werden.') return False else: logging.info('Time entry was created successfully') self.delete_event(data[0]) self.clear_screen() self.print_time_entry(STARTTIME=data[1], ENDTIME=endtime, ACTIVITY=action) print('Zeiteintrag wurde gespeichert.') printtext = [ 'Nächsten Zeiteintrag beginnen ?', '[1] Ja', '[0] Nein' ] userinput = self.userchoise(printtext) self.clear_screen() if userinput == 1: self.time_start() elif userinput == 2: logging.info('Event should be deleted (eventid: {})'.format(data[0])) self.delete_event(data[0]) ## Interactive mode #================== ### Main menu of the interactive menu def start_interactive_mode(self): self.clear_screen() printtext = [ '=> Hauptmenü - Angemeldet als {}'.format(self.USERNAME), 'Was willst du tun?', '[1] Zeiterfassung starten', '[2] heutiger Report', '[3] Benutzerverwaltung', '[0] Programm verlassen' ] userinput = self.userchoise(printtext, 4) if userinput == 1: logging.debug('Start TimeTrack') self.time_start() elif userinput == 2: logging.info('Print todays report') self.report_by_day(DETAIL=True) input() elif userinput == 3: self.interactive_usermanagment() elif userinput == 4: print('commig soon ...') input() else: exit() self.start_interactive_mode() def interactive_usermanagment(self): self.clear_screen() printtext = [ '==> Benutzerverwaltung - Angemeldet als {}'.format(self.USERNAME), 'Was willst du tun?', '[1] Benutzer anlegen', '[2] Benutzer wechseln', '[3] Benutzer löschen', '[0] abbrechen' ] userinput = self.userchoise(printtext, 4) if userinput == 1: self.create_user() elif userinput == 2: pass elif userinput == 3: pass ## Reports #========= ### One day report, optionally with time entries def report_by_day(self, DATE=datetime.date.today(), USER='', DETAIL=''): if not USER: USER = self.USERID timedata = self.get_time_entry(DAY=DATE, USERID=USER) STARTTIMES = [] ENDTIMES = [] DURATIONS = [] for entry in timedata: STARTTIMES.append(entry[1]) ENDTIMES.append(entry[2]) DURATIONS.append(entry[2] - entry[1]) if timedata: FIRSTSTARTTIME = min(STARTTIMES) LASTENDTIME = max(ENDTIMES) TRACKEDTIMESPAN = (LASTENDTIME - FIRSTSTARTTIME) TRACKEDTIME = sum(DURATIONS, datetime.timedelta()) NONTRACKEDTIME = (TRACKEDTIMESPAN - TRACKEDTIME) ENTRIECOUNT = len(timedata) self.clear_screen() text = "{:40} {}" print(text.format("Beginn des ersten Zeiteintrags:", FIRSTSTARTTIME.strftime('%d.%m.%Y %H:%M'))) print(text.format("Ende des letzen Zeiteintrags:", LASTENDTIME.strftime('%d.%m.%Y %H:%M'))) print(text.format("Maximal erfassbare Zeit:", self.timedela_to_string(TRACKEDTIMESPAN))) print(text.format("Nicht erfasste Zeit:", self.timedela_to_string(NONTRACKEDTIME))) print(text.format("Erfasste Zeit:", self.timedela_to_string(TRACKEDTIME))) print(text.format("Zeiteinträge:", ENTRIECOUNT)) printtext = [ 'Zeiteinträge anzeigen?', '[1] Ja', '[0] Nein' ] userinput = self.userchoise(printtext) if userinput == 1: for entry in timedata: self.print_time_entry(entry[1], entry[2], entry[4]) ## Outputs #========= ### Shows a time entry def print_time_entry(self, STARTTIME='', ENDTIME='', ACTIVITY=''): s = (ENDTIME - STARTTIME).seconds hours, remainder = divmod(s, 3600) minutes, seconds = divmod(remainder, 60) print(50*"-") print('Start: {} Uhr'.format(STARTTIME.strftime('%H:%M'))) print('Ende: {} Uhr'.format(ENDTIME.strftime('%H:%M'))) print('Dauer: {:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds))) if not ACTIVITY == '': print('Aktivität:') print(ACTIVITY) print(50*"-") ## Miscellaneous #=============== ### User selection def userchoise(self, TEXT='', MAX=2): userinput = -1 while not -1 < int(userinput) < MAX: for text in TEXT: print(text) userinput = input('Aktion: ') logging.debug('User input: {}'.format(userinput)) try: int(userinput) except ValueError: userinput = -1 else: self.clear_screen() return int(userinput) ### Conversion to string of Timedelta typ def timedela_to_string(self, TIME): s = TIME.seconds hours, remainder = divmod(s, 3600) minutes, seconds = divmod(remainder, 60) output = '{:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds)) return output if __name__ == "__main__": test = TimeTrack() test.start_interactive_mode() test.DBCON.close()