Exemple #1
0
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...')
Exemple #2
0
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...')
Exemple #3
0
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)
Exemple #4
0
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
Exemple #5
0
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])
Exemple #6
0
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)
Exemple #7
0
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',
Exemple #8
0
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
Exemple #9
0
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...')
Exemple #10
0
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)