def stop(password): """ Stop clocking on previous task. If this is instance task then ask for more information """ if password is None: password = get_pass() check_config() with Settings() as config: client = Client(username=config['username'], password=password, database=config['database'], host=config['host']) client.connect() filters = [ ('user_id', '=', client.user.id), ] employee = client.search_read('hr.employee', filters) if not employee[0]['current_task']: click.echo('Nothing to show. Exiting..') return ids = employee[0]['current_task'][0] client.terminate_tracking([ids]) task = Task() task.id = ids click.launch(task.url()) input('Press Enter to continue...')
def search(password, search_term): """Return tasks in priority order. Default is to find your tasks. This can also be used to fetch tasks by user. """ if password is None: password = get_pass() check_config() with Settings() as config: client = Client(username=config['username'], password=password, database=config['database'], host=config['host']) client.connect() if not search_term: search_term = click.prompt('Search') tasks = Task.search(client, search_term) if len(tasks) != 0: click.echo(click.style(Task().print_topic('terminal'), fg='blue')) for task in tasks: click.echo(click.style(task.as_formatted('terminal'), fg='blue')) else: click.echo('No tasks found') input('Press Enter to continue...')
def project(password, list_projects, project, summary, sub_tasks): """ Return active projects""" if not list_projects and (not project and not summary): click.echo("Select --list-projects or --project/--summary") return if password is None: password = get_pass() check_config() with Settings() as config: client = Client(username=config['username'], password=password, database=config['database'], host=config['host']) client.connect() filters = [] if not sub_tasks: filters.append(('is_subtask_project', '=', False)) projects = client.search_read('project.project', filters) if list_projects: for project in projects: click.echo(f'{project["id"]}\t{project["display_name"]}') return if summary: for project in projects: # TODO change to single search for one project. if project['id'] == int(summary): print_project_page(client, project, 10) return if project: for pro in projects: # TODO change to single search for one project. if pro['id'] == int(project): print_project_page(client, pro)
def tasks(password, user, interactive): """Return tasks in priority order. Default is to find your tasks. This can also be used to fetch tasks by user. """ if password is None: password = get_pass() check_config() with Settings() as config: client = Client(username=config['username'], password=password, database=config['database'], host=config['host']) client.connect() click.echo('Fetching tasks from ODOO... This may take a while.', file=sys.stderr) if not user: user_id = client.user.id filters = [ ('user_id', '=', user_id), ('stage_id', '!=', 8) # This is done stage. Should be in config? ] all_tasks = Task.fetch_tasks(client, filters) all_sorted = sorted(all_tasks, key=lambda x: x.priority, reverse=True) if not interactive: click.echo(Task.print_topic()) for task in all_sorted: click.echo(task) else: current_index = 0 # Loop with index as interactive can go both ways while True: task = all_sorted[current_index] index_mod = as_interactive(client=client, task=task, task_count=len(all_sorted), current_index=current_index) new_index = current_index + index_mod current_index = new_index if current_index >= len(all_sorted): current_index = 0 if current_index < 0: current_index = len(all_sorted) - 1
def instant(password): """ Start clocking on new task """ if password is None: password = get_pass() check_config() with Settings() as config: client = Client(username=config['username'], password=password, database=config['database'], host=config['host']) client.connect() task = Task() # Inbox project 1555 task.project_id = 1555 task.name = f'Task started on {datetime.datetime.now()}' task.description = "Fill this later" task.user_id = client.user.id ids = task.create(client) client.start_tracking([ids])
def attendance(password, user, period, start=None, end=None): """ Retrieves timesheet and totals it for the current month. """ from datetime import datetime import pytz import holidays def colored_diff(title, diff, notes=None, invert=False): positive_color = 'green' negative_color = 'magenta' if invert: positive_color = 'magenta' negative_color = 'green' if not notes: notes = '' else: notes = f' ! {notes}' color = negative_color if diff[0] == '-' else positive_color click.echo( click.style(f'{title}\t', fg='blue') + click.style(diff, fg=color) + click.style(notes, fg='magenta')) if password is None: password = get_pass() check_config() with Settings() as config: client = Client(username=config['username'], password=password, database=config['database'], host=config['host']) client.connect() if not user: user_id = client.user.id filters = [('employee_id.user_id.id', '=', user_id)] # Add the start filter if start: filters.append(('check_in', '>=', start.strftime('%Y-%m-%d 00:00:00'))) elif period == 'month': filters.append( ('check_in', '>=', datetime.now().strftime('%Y-%m-01 00:00:00'))) elif period == 'year': filters.append( ('check_in', '>=', datetime.now().strftime('%Y-01-01 00:00:00'))) # Add optional end filter if end: filters.append(('check_out', '<', end.strftime('%Y-%m-%d 00:00:00'))) attendance_ids = client.search('hr.attendance', filters) attendances = client.read('hr.attendance', attendance_ids) weeks = {} # @TODO Assumes user is in Finland local_holidays = holidays.FI() # Faux data to test holidays # attendances.append({ # 'check_in': '2018-01-01 00:00:00', # 'check_out': '2018-01-01 02:00:00', # 'worked_hours': 2 # }) for attendance in attendances: # Get a localized datetime object # @TODO This assumes the server returns times as EU/Helsinki date = pytz.timezone('Europe/Helsinki').localize( datetime.strptime(attendance['check_in'], '%Y-%m-%d %H:%M:%S')) # If there is no checkout time, sum to now if attendance['check_out'] == False: # @TODO Same as above now = pytz.timezone('Europe/Helsinki').localize(datetime.utcnow()) attendance['worked_hours'] = (now - date).seconds / 3600 # Get the day and week index keys (Key = %Y-%m-%d) day_key = date.strftime('%Y-%m-%d') # Counts weeks from first Monday of the year week_key = date.strftime('%W') if week_key not in weeks: weeks[week_key] = {} if day_key not in weeks[week_key]: # @TODO Assumes 7.5 hours per day weeks[week_key][day_key] = { 'allocated_hours': 7.5, 'worked_hours': 0, 'holiday': None } if day_key in local_holidays: # This day is a holiday, no allocated hours weeks[week_key][day_key]['holiday'] = local_holidays.get(day_key) weeks[week_key][day_key]['allocated_hours'] = 0 # Sum the attendance weeks[week_key][day_key]['worked_hours'] += attendance['worked_hours'] total_diff = 0 total_hours = 0 day_diff = 0 click.echo( click.style( f'Balance as of {(datetime.today().isoformat(timespec="seconds"))} (system time)', fg='blue')) click.echo(click.style('Day\t\tWorked\tDifference', fg='blue')) for week_number, week in sorted(weeks.items()): for key, day in sorted(week.items()): diff = day['worked_hours'] - day['allocated_hours'] colored_diff(f'{key}\t{(day["worked_hours"]):.2f}', f'{diff:+.2f}', day['holiday']) if key == datetime.today().strftime('%Y-%m-%d'): day_diff += day['worked_hours'] - day['allocated_hours'] else: total_diff += day['worked_hours'] - day['allocated_hours'] total_hours += day['worked_hours'] today = datetime.now().strftime('%Y-%m-%d') this_week = datetime.now().strftime('%W') hours_today = 0 allocated_today = 0 if today in weeks.get(this_week, {}): hours_today = weeks[this_week][today]['worked_hours'] allocated_today = weeks[this_week][today]['allocated_hours'] click.echo(click.style('---\t\t------\t-----', fg='blue')) colored_diff(f'Totals:\t\t{total_hours:.2f}', f'{(total_diff + day_diff):+.2f}') print() colored_diff('Balance yesterday:', f'{total_diff:+.2f}') colored_diff('Balance now:\t', f'{(total_diff + day_diff):+.2f}') colored_diff('Allocated hours today:', f'{(allocated_today - hours_today):+.2f}', invert=True)
import keyring from odoohelper.client import Client from odoohelper.settings import Settings from odoohelper.tasks import Task, tasks_group from odoohelper.utils import get_pass, validate_odoo_date, check_config @click.group() def attendance_group(): # Collection for attendance commands pass @attendance_group.command() @click.password_option(prompt=True if get_pass() is None else False, confirmation_prompt=False) @click.option('-u', '--user', metavar='<user full name>', help="User display name in Odoo") @click.option('--month', 'period', flag_value='month', default=True, help="Show records since start of current month") @click.option('--year', 'period', flag_value='year', help="Show records since start of current year") @click.option('--start',
def tasks(password, user, interactive, list_tasks, print_format, start=None, end=None): """Return tasks in priority order. Default is to find your tasks. This can also be used to fetch tasks by user. """ if password is None: password = get_pass() check_config() with Settings() as config: client = Client(username=config['username'], password=password, database=config['database'], host=config['host']) client.connect() click.echo('Fetching tasks from ODOO... This may take a while.', file=sys.stderr) if not user: user_id = client.user.id else: selected_user = None while not selected_user: filters = [] filters.append(('name', 'ilike', user)) users = client.search_read('res.users', filters) if len(users) == 1: selection = 0 else: for index, user_data in enumerate(users): click.echo(f'[{index}] {user_data["name"]}') click.echo(f'[s] Search again') selection = click.prompt('Select user') try: selected = int(selection) selected_user = users[selected] except: user = click.prompt('User') user_id = selected_user['id'] filters = [ ('user_id', '=', user_id), ('stage_id', '!=', 8), # This is done stage. Should be in config? ] if start: filters.append(('date_start', "<=", end.strftime('%Y-%m-%d 00:00:00'))) if end: filters.append( ('date_deadline', "<=", end.strftime('%Y-%m-%d 23:59:00'))) all_tasks = Task.fetch_tasks(client, filters) all_sorted = sorted(all_tasks, key=lambda x: x.priority, reverse=True) if not interactive: click.echo(Task.print_topic(print_format)) for task in all_sorted: click.echo(task.as_formatted(print_format)) else: current_index = 0 # Loop with index as interactive can go both ways while True: task = all_sorted[current_index] index_mod = as_interactive(client=client, task=task, task_count=len(all_sorted), current_index=current_index) new_index = current_index + index_mod current_index = new_index if current_index >= len(all_sorted): current_index = 0 if current_index < 0: current_index = len(all_sorted) - 1
def create(password, title): """ Create new task """ if password is None: password = get_pass() check_config() with Settings() as config: client = Client(username=config['username'], password=password, database=config['database'], host=config['host']) client.connect() message = create_message() selected_project = None while not selected_project: project = click.prompt('Project') filters = [] filters.append(('is_subtask_project', '=', False)) filters.append(('name', 'ilike', project)) projects = client.search_read('project.project', filters) for index, project_data in enumerate(projects): click.echo(f'[{index}] {project_data["display_name"]}') click.echo(f'[s] Search again') if len(projects) == 1: selection = click.prompt('Select project', default=0) else: selection = click.prompt('Select project') try: selected = int(selection) selected_project = projects[selected] except: pass task = Task() task.name = title task.description = message task.project_id = selected_project['id'] selected_user = None while not selected_user: user = click.prompt('User') filters = [] filters.append(('name', 'ilike', user)) users = client.search_read('res.users', filters) for index, user_data in enumerate(users): click.echo(f'[{index}] {user_data["name"]}') click.echo(f'[s] Search again') if len(users) == 1: selection = click.prompt('Select user', default=0) else: selection = click.prompt('Select user') try: selected = int(selection) selected_user = users[selected] except: pass task.user_id = selected_user['id'] task.create(client) click.echo(task.url()) input('Press Enter to continue...')
def attendance(password, user, period, start=None, end=None): """ Retrieves timesheet and totals it for the current month. """ from datetime import datetime, timedelta import pytz import holidays def colored_diff(title, diff, notes=None, invert=False): positive_color = 'green' negative_color = 'magenta' if invert: positive_color = 'magenta' negative_color = 'green' if not notes: notes = '' else: notes = f' ! {notes}' color = negative_color if diff[0] == '-' else positive_color click.echo( click.style(f'{title}\t', fg='blue') + click.style(diff, fg=color) + click.style(notes, fg='magenta')) if password is None: password = get_pass() check_config() with Settings() as config: client = Client(username=config['username'], password=password, database=config['database'], host=config['host']) client.connect() if not user: user_id = client.user.id filters = [('employee_id.user_id.id', '=', user_id)] filters_leave = [('employee_id.user_id.id', '=', user_id), ('holiday_type', '=', 'employee')] # Add end cutoff for checkout if there is one if end: filters.append(('check_out', '<', end.strftime('%Y-%m-%d 00:00:00'))) # Get start and end times if start: # No need to calculate start or end pass elif period == 'month': # Calculate month start = datetime.now().replace(day=1, hour=0, minute=0, second=0) if start.month < 12: end = start.replace(month=start.month + 1, day=1) - \ timedelta(days=1) else: end = start.replace(day=31) elif period == 'year': # Calculate year start = datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0) end = start.replace(month=start.month, day=31) # Add start filters filters.append(('check_in', '>=', start.strftime('%Y-%m-%d 00:00:00'))) filters_leave.append( ('date_from', '>=', start.strftime('%Y-%m-%d 00:00:00'))) # Always set end to end of today if not set if not end: end = datetime.now().replace(hour=23, minute=59, second=59) # Add end cutoff for leaves filters_leave.append(('date_to', '<', end.strftime('%Y-%m-%d 00:00:00'))) attendance_ids = client.search('hr.attendance', filters) attendances = client.read('hr.attendance', attendance_ids) leave_ids = client.search('hr.holidays', filters_leave) leaves = client.read('hr.holidays', leave_ids) def daterange(start_date, end_date): for n in range(int((end_date - start_date).days)): yield start_date + timedelta(n) # Pre-process the weeks and days weeks = {} # @TODO Assumes user is in Finland local_holidays = holidays.FI() # Faux data to test holidays # attendances.append({ # 'check_in': '2018-01-01 00:00:00', # 'check_out': '2018-01-01 02:00:00', # 'worked_hours': 2 # }) # Process attendances for attendance in attendances: # Get a localized datetime object # @TODO This assumes the server returns times as EU/Helsinki date = pytz.timezone('Europe/Helsinki').localize( datetime.strptime(attendance['check_in'], '%Y-%m-%d %H:%M:%S')) # If there is no checkout time, sum to now if attendance['check_out'] == False: # @TODO Same as above now = pytz.timezone('Europe/Helsinki').localize(datetime.utcnow()) attendance['worked_hours'] = (now - date).seconds / 3600 # Get the day and week index keys (Key = %Y-%m-%d) day_key = date.strftime('%Y-%m-%d') # Counts weeks from first Monday of the year week_key = date.strftime('%W') if week_key not in weeks: weeks[week_key] = {} if day_key not in weeks[week_key]: # @TODO Assumes 7.5 hours per day weeks[week_key][day_key] = { 'allocated_hours': 7.5, 'worked_hours': 0, 'overtime': False, 'overtime_reason': None } # Sum the attendance weeks[week_key][day_key]['worked_hours'] += attendance['worked_hours'] for date in daterange(start, end): # Get the day and week index keys (Key = %Y-%m-%d) day_key = date.strftime('%Y-%m-%d') # Counts weeks from first Monday of the year week_key = date.strftime('%W') if day_key not in weeks.get(week_key, {}): # We don't care, no attendances for this day continue if day_key in local_holidays: # This day is a holiday, no allocated hours weeks[week_key][day_key]['overtime'] = True weeks[week_key][day_key]['overtime_reason'] = local_holidays.get( day_key) weeks[week_key][day_key]['allocated_hours'] = 0 if date.isoweekday() >= 6: # Weekend, assume everything is overtime weeks[week_key][day_key]['overtime'] = True weeks[week_key][day_key]['overtime_reason'] = 'Weekend' weeks[week_key][day_key]['allocated_hours'] = 0 # Process any leaves for leave in leaves: leave_start = pytz.timezone('Europe/Helsinki').localize( datetime.strptime(leave['date_from'], '%Y-%m-%d %H:%M:%S')) leave_end = pytz.timezone('Europe/Helsinki').localize( datetime.strptime(leave['date_to'], '%Y-%m-%d %H:%M:%S')) for date in daterange(leave_start, leave_end): # Get the day and week index keys (Key = %Y-%m-%d) day_key = date.strftime('%Y-%m-%d') # Counts weeks from first Monday of the year week_key = date.strftime('%W') if day_key not in weeks.get(week_key, {}): # We don't care, no attendances for this day continue weeks[week_key][day_key]['overtime'] = True weeks[week_key][day_key][ 'overtime_reason'] = f'Leave: {leave["name"]}' weeks[week_key][day_key]['allocated_hours'] = 0 total_diff = 0 total_hours = 0 day_diff = 0 click.echo( click.style( f'Balance as of {(datetime.today().isoformat(timespec="seconds"))} (system time)', fg='blue')) click.echo(click.style('Day\t\tWorked\tDifference', fg='blue')) for week_number, week in sorted(weeks.items()): for key, day in sorted(week.items()): if day['worked_hours'] == 0.0: continue diff = day['worked_hours'] - day['allocated_hours'] colored_diff(f'{key}\t{(day["worked_hours"]):.2f}', f'{diff:+.2f}', day.get('overtime_reason', None)) if key == datetime.today().strftime('%Y-%m-%d'): day_diff += day['worked_hours'] - day['allocated_hours'] else: total_diff += day['worked_hours'] - day['allocated_hours'] total_hours += day['worked_hours'] today = datetime.now().strftime('%Y-%m-%d') this_week = datetime.now().strftime('%W') hours_today = 0 allocated_today = 0 if today in weeks.get(this_week, {}): hours_today = weeks[this_week][today]['worked_hours'] allocated_today = weeks[this_week][today]['allocated_hours'] click.echo(click.style('---\t\t------\t-----', fg='blue')) colored_diff(f'Totals:\t\t{total_hours:.2f}', f'{(total_diff + day_diff):+.2f}') print() colored_diff('Balance yesterday:', f'{total_diff:+.2f}') colored_diff('Balance now:\t', f'{(total_diff + day_diff):+.2f}') colored_diff('Allocated hours today:', f'{(allocated_today - hours_today):+.2f}', invert=True)