Fill hours worked in SAP Netweaver Automatically

Continuing the theme of automation, one of the most repetitive tasks if you work for a big company is timesheets. So I set out to rectify this by scripting it!

Start with you configuration, I named mine hours.ini:

[DEFAULT]
url = FILL_ME_IN
username = FILL_ME_IN
password = FILL_ME_IN

then we need the magic of Selenium to do the heavy lifting, so we install it:

$ pip3 install selenium

I called this script, unsurprisingly hours.py:

#!/usr/bin/env python3
"""
hours.py by Neil Grogan, 2015

A script to fill in hours automatically in SAP NetWeaver Portal
"""
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import configparser
import os

TIMEOUT = 5

def main():
    # Setup
    config = read_config()
    driver = load_website(config['url'], "SAP NetWeaver Portal")
    wait = WebDriverWait(driver, TIMEOUT)

    # Action
    login_to_website(driver, config['username'], config['password'])
    wait_for_timesheet_table(driver, wait)

    fill_in_timesheet(driver)
    submit_and_confirm_timesheet(driver, wait)

    logoff_and_close(driver, wait)

def read_config():
    config = configparser.ConfigParser()
    path = os.path.dirname(os.path.realpath(__file__))
    config.read(path + '/hours.ini')
    return config['DEFAULT']

def load_website(url, title):
    driver = webdriver.Firefox()
    driver.get(url)
    assert title in driver.title
    return driver

def login_to_website(driver, username, password):
    elem = driver.find_element_by_id("logonuidfield")
    elem.send_keys(username)
    elem = driver.find_element_by_id("logonpassfield")
    elem.send_keys(password)
    elem.send_keys(Keys.RETURN)

def wait_for_timesheet_table(driver, wait):
    # wait for ajax items to load
    elem = wait.until(EC.element_to_be_clickable((By.ID, 'L2N2')))
    elem.click()

    wait.until(EC.frame_to_be_available_and_switch_to_it('contentAreaFrame'))
    wait.until(EC.frame_to_be_available_and_switch_to_it('isolatedWorkArea'))

    # Wait until timesheet appears
    elem = wait.until(EC.presence_of_element_located(
        (By.ID, "aaaaKEBH.VcCatTableWeek.WORKDATE1_InputField.0")))

def fill_in_timesheet(driver):
    # Fill in days of the week with 8 hours each
    for i in range(1,6):
        elem = driver.find_element_by_id("aaaaKEBH.VcCatTableWeek.WORKDATE"+str(i)+"_InputField.0")
        elem.click()
        elem.clear()
        elem.send_keys('8')

def submit_and_confirm_timesheet(driver, wait):
    # Wait for javascript to digest our fast hours entry :)
    time.sleep(0.2)

    # Start to Click Review Button
    elem = driver.find_element_by_id("aaaaKEBH.VcCatRecordEntryView.ButtonNext")
    elem.click()

    # Click Save/Confirm
    elem = wait.until(EC.presence_of_element_located(
        (By.ID, "aaaaLBOD.VcGenericButtonView.Save_com_sap_xss_hr_cat_record_vac_review_VcCatRecordReview")))
    elem.click()

    # Wait until confirmation of save
    elem = wait.until(EC.presence_of_element_located(
        (By.ID, "aaaaLMJA.WDMsgBox.MessageArea-txt")))
    assert "Your data has been saved" or "No data was changed" in elem.text

def logoff_and_close(driver, wait):
    # Start logging off
    driver.switch_to.default_content() # get back out of iframe
    driver.find_element_by_css_selector("#buttonlogoff > span.button_inner").click() # Click logoff
    driver.find_element_by_css_selector("div.button_middle").click() # Click yes to logoff
    wait.until(lambda driver:driver.title.lower().startswith('home'))
    driver.close()
    
if __name__ == '__main__':
    main()