def test_get_list_of_week_start_dates_in_range__one_week(self): service = DateTimeService() start_date = date(2016, 5, 8) end_date = date(2016, 5, 8) result = service.get_list_of_week_start_dates_in_range( start_date, end_date) self.assertEqual(type(result), list) self.assertEqual(len(result), 1) self.assertEqual(result[0], date(2016, 5, 8))
def test_get_list_of_week_start_dates_in_range__two_weeks_at_end(self): service = DateTimeService() start_date = date(2016, 5, 14) end_date = date(2016, 5, 21) result = service.get_list_of_week_start_dates_in_range( start_date, end_date) self.assertEqual(type(result), list) self.assertEqual(len(result), 2) self.assertEqual(result[0], date(2016, 5, 8)) self.assertEqual(result[1], date(2016, 5, 15))
def test_get_list_of_week_start_dates_in_range__three_weeks_at_middel( self): service = DateTimeService() start_date = date(2016, 5, 10) end_date = date(2016, 5, 25) result = service.get_list_of_week_start_dates_in_range( start_date, end_date) self.assertEqual(type(result), list) self.assertEqual(len(result), 3) self.assertEqual(result[0], date(2016, 5, 8)) self.assertEqual(result[1], date(2016, 5, 15)) self.assertEqual(result[2], date(2016, 5, 22))
def get(self, request, pk, from_year, from_month, from_day, to_year, to_month, to_day, format=None): comp = self._get_company_info(pk) week_start_date = date(year=int(from_year), month=int(from_month), day=int(from_day)) end_week_start_date = date(year=int(to_year), month=int(to_month), day=int(to_day)) book = xlwt.Workbook(encoding='utf8') sheet = book.add_sheet('Timesheet') time_tracking_service = TimeTrackingService() submitted_sheets = time_tracking_service.get_company_users_submitted_work_timesheet_by_week_range( comp.id, week_start_date, end_week_start_date) self._write_headers(sheet) row_num = 1 # Now enumerate every single week within the specified # time range and delegate to the writing method to # determine what to write out # i.e. even if the retrieved submitted time sheets do # not contain data for a week, we should still attempt # to run through that week, and let the logic to decide # what to write. e.g. default weekly total for fulltime # employees, and empty for part time workers, etc. date_time_service = DateTimeService() week_start_dates = date_time_service.get_list_of_week_start_dates_in_range(week_start_date, end_week_start_date) for week_start_date in week_start_dates: timesheet = submitted_sheets.get(week_start_date, []) row_num = self._write_company(row_num, comp, week_start_date, sheet, timesheet) response = HttpResponse(content_type='application/vnd.ms-excel') # Need company name: response['Content-Disposition'] = ( 'attachment; filename={0}_employee_worktime_report_{1}.xls' ).format(comp, week_start_date.strftime('%m_%d_%Y')) book.save(response) return response
class CompanyUsersTimePunchCardWeeklyReportV2View(ExcelExportViewBase): date_time_service = DateTimeService() time_punch_card_service = TimePunchCardService() company_personnel_service = CompanyPersonnelService() def __init__(self): # List out instance variables that will be used # below self._company = None self._week_start_date = None self._week_end_date = None self._employee_list_cache = None self._blank_state_sheet_data_template = None def _build_employee_info_cache(self): self._employee_list_cache = [] all_employees = self._get_all_employee_users_for_company(self._company.id) filtered_employee_user_ids = self.company_personnel_service.get_company_employee_user_ids_non_fully_terminated_in_time_range( self._company.id, self._week_start_date, self._week_end_date ) for employee in all_employees: if (employee.id in filtered_employee_user_ids): self._employee_list_cache.append({ 'user_id': employee.id, 'full_name': self._get_user_full_name(employee) }) def _build_report_time_sheets_data(self): # The structure of the result would be a nested dictionary # result{ state: { card_type: { user_id: { weekday_index: hours } } } } all_state_sheets = {} # First read in all employee submitted cards all_cards = self._get_all_punch_cards() # Now setup blank/base sheet data for all states # encountered if (len(all_cards) <= 0): state = 'No State' all_state_sheets.setdefault( state, self._get_blank_sheet_data()) else: for card in all_cards: state = card.state if (not state): state = TIME_PUNCH_CARD_NON_SPECIFIED_STATE if (state not in all_state_sheets): all_state_sheets.setdefault( state, self._get_blank_sheet_data()) # Now "merge" user submitted data into the sheets # data for punch_card in all_cards: self._merge_punch_card_data_to_full_set( punch_card, all_state_sheets) return all_state_sheets def _get_all_punch_cards(self): return self.time_punch_card_service.get_company_users_time_punch_cards_by_date_range( self._company.id, self._week_start_date, self._week_end_date) def _get_blank_sheet_data(self): # Cache a copy as template and return deep copies of that # to save some computational power if (not self._blank_state_sheet_data_template): sheet_data = {} for card_type in CARD_TYPES: sheet_data.setdefault( card_type, self._get_blank_card_type_section_data(card_type)) self._blank_state_sheet_data_template = sheet_data return deepcopy(self._blank_state_sheet_data_template) def _get_blank_card_type_section_data(self, card_type): card_type_section_data = {} card_type_behavior = CARD_TYPES[card_type] if card_type_behavior.get('PrePopulate', True): for user_info in self._employee_list_cache: card_type_section_data.setdefault( user_info['user_id'], self._get_employee_weekly_blank_data()) return card_type_section_data def _get_employee_weekly_blank_data(self): blank_data = {} for isoweekday in range(7): blank_data.setdefault(isoweekday, 0.0) return blank_data def _merge_punch_card_data_to_full_set(self, punch_card, all_states_sheets_data): state_data = None if (punch_card.state): state_data = all_states_sheets_data[punch_card.state] else: state_data = all_states_sheets_data[TIME_PUNCH_CARD_NON_SPECIFIED_STATE] card_type = punch_card.card_type if (card_type in CARD_TYPES): index_card_type = CARD_TYPES[card_type].get('MergeWith', card_type) card_type_data = state_data[index_card_type] if punch_card.user_id not in card_type_data: card_type_data.setdefault( punch_card.user_id, self._get_employee_weekly_blank_data()) employee_weekly_data = card_type_data[punch_card.user_id] card_weekday_iso = punch_card.get_card_day_of_week_iso() if (CARD_TYPES[card_type].get('NoHours', True) or not punch_card.start or not punch_card.end): # Even if an employee filed 2 of such cards in one slot # Only count hours once if (employee_weekly_data[card_weekday_iso] <= 0): employee_weekly_data[card_weekday_iso] = TIME_PUNCH_CARD_NO_HOURS_DEFAULT_HOURS else: # Accumulate the hours specified by the card hours = punch_card.get_punch_card_hours() employee_weekly_data[card_weekday_iso] += hours def _write_all_states_sheets(self, all_states_sheets_data): for state in all_states_sheets_data: self._write_state_sheet(state, all_states_sheets_data[state]) def _write_state_sheet(self, state, state_sheet_data): self._start_work_sheet(state) self._write_sheet_headers(state) for card_type in CARD_TYPES: if CARD_TYPES[card_type].get('RenderRow', True): self._write_card_type_section(card_type, state_sheet_data[card_type]) def _write_sheet_headers(self, state): self._write_cell('Company') self._write_cell(self._company.name) self._next_row() self._write_cell('Payroll Sheet') self._write_cell(self._week_start_date.strftime(DATE_FORMAT_STRING)) self._write_cell(self._week_end_date.strftime(DATE_FORMAT_STRING)) self._next_row() self._write_cell('State') self._write_cell(state) self._next_row() self._next_row() def _write_card_type_section(self, card_type, card_type_section_data): card_type_behavior = CARD_TYPES[card_type] self._write_cell(card_type_behavior['name']) self._next_row() self._write_card_type_section_headers() # Now write the data for user_info in self._employee_list_cache: user_id = user_info['user_id'] if (user_id in card_type_section_data): self._write_employee_weekly_data( user_info, card_type_section_data[user_id]) self._next_row() def _write_card_type_section_headers(self): self._write_cell('Employee Name') for i in range(7): date = self._week_start_date + timedelta(i) header_text = '{} - {}'.format(date.strftime('%A'), date.strftime(DATE_FORMAT_STRING)) self._write_cell(header_text) self._next_row() def _write_employee_weekly_data(self, user_info, employee_weekly_data): self._write_cell(user_info['full_name']) for weekdayiso in range(7): self._write_cell(employee_weekly_data[weekdayiso]) self._next_row() ''' Get the Weekly Time Punch Card report excel of a company's all employees ''' @user_passes_test(company_employer) def get(self, request, pk, year, month, day, format=None): # Parse all information needed from URL self._company = self._get_company_info(pk) input_date = date(year=int(year), month=int(month), day=int(day)) week_range = self.date_time_service.get_week_range_by_date(input_date) self._week_start_date = week_range[0] self._week_end_date = week_range[1] # First collect and cache company employees data self._build_employee_info_cache() # Now collect time punch data for the given week # and organize them to the right hierarchy all_states_sheets_data = self._build_report_time_sheets_data() # Now write out all data self._init() self._write_all_states_sheets(all_states_sheets_data) response = HttpResponse(content_type='application/vnd.ms-excel') response['Content-Disposition'] = ( 'attachment; filename={0}_employee_worktime_report_{1}.xls' ).format(self._company, self._week_start_date.strftime('%m_%d_%Y')) self._save(response) return response
class TimeOffRecord(object): hash_key_service = HashKeyService() date_time_service = DateTimeService() def __init__(self, time_off_domain_model): if (time_off_domain_model is None): raise ValueError('Must pass valid time off domain model.') # List out instance variables self.requestor_user_id = None self.requestor_user_info = None self.approver_user_id = None self.approver_user_info = None self.start_date_time = None self.duration = None self.status = None self.decision_timestamp = None self.record_type = None self.request_timestamp = None # Parse requestor info requestor_user_descriptor = time_off_domain_model['requestor'][ 'personDescriptor'] self.requestor_user_id = int( self.hash_key_service.decode_key_with_environment( requestor_user_descriptor)) if (self.requestor_user_id): user_model = User.objects.get(pk=self.requestor_user_id) self.requestor_user_info = UserInfo(user_model) # Parse approver info if ('approver' in time_off_domain_model and time_off_domain_model['approver']): approver_user_descriptor = time_off_domain_model['approver'][ 'personDescriptor'] self.approver_user_id = int( self.hash_key_service.decode_key_with_environment( approver_user_descriptor)) if (self.approver_user_id): user_model = User.objects.get(pk=self.approver_user_id) self.approver_user_info = UserInfo(user_model) # Parse record type self.record_type = time_off_domain_model['type'] # Parse Duration self.duration = float(time_off_domain_model['duration']) # Parse status self.status = time_off_domain_model['status'] # Parse all dates and times to objects self.start_date_time = self.date_time_service.parse_date_time( time_off_domain_model['startDateTime']) self.request_timestamp = self.date_time_service.parse_date_time( time_off_domain_model['requestTimestamp']) if ('decisionTimestamp' in time_off_domain_model): decision_time_str = time_off_domain_model['decisionTimestamp'] self.decision_timestamp = self.date_time_service.parse_date_time( decision_time_str) @property def requestor_full_name(self): if (self.requestor_user_info is None): return None return self.requestor_user_info.full_name @property def approver_full_name(self): if (self.approver_user_info is None): return None return self.approver_user_info.full_name
def _get_last_week_start_date(self): date_time_service = DateTimeService() return date_time_service.get_last_week_range_by_date(date.today())[0]
class TimePunchCard(object): hash_key_service = HashKeyService() date_time_service = DateTimeService() def __init__(self, punch_card_domain_model): if (punch_card_domain_model is None): raise ValueError('Must pass valid time punch card domain model.') # List out instance variables self.user_id = None self.user_info = None self.date = None self.start = None self.end = None self.state = None self.card_type = None self.in_progress = None self.system_stopped = None # Parse out user ID user_descriptor = punch_card_domain_model['employee'][ 'personDescriptor'] self.user_id = int( self.hash_key_service.decode_key_with_environment(user_descriptor)) # Parse card type self.card_type = punch_card_domain_model.get('recordType') # Parse all dates and times to objects self.date = self.date_time_service.parse_date_time( punch_card_domain_model['date']) start_str = punch_card_domain_model.get('start') if (start_str): self.start = self.date_time_service.parse_date_time(start_str) end_str = punch_card_domain_model.get('end') if (end_str): self.end = self.date_time_service.parse_date_time(end_str) # Parse attributes attributes = punch_card_domain_model.get('attributes') if (attributes): for attribute in attributes: # For now only cares about state if attribute['name'] == PUNCH_CARD_ATTRIBUTE_TYPE_STATE: self.state = attribute['value'] break in_progress_str = punch_card_domain_model.get('inProgress') if (in_progress_str): self.in_progress = bool(in_progress_str) system_stopped_str = punch_card_domain_model.get('systemStopped') if (system_stopped_str): self.system_stopped = bool(system_stopped_str) # Support lasy-evaluated validation self._validation_issues = None def get_punch_card_hours(self): raw_hours = self.__get_raw_card_hours() if self.card_type == PUNCH_CARD_TYPE_BREAK_TIME: return -raw_hours return raw_hours def __get_raw_card_hours(self): if (self.start is not None and self.end is not None): card_hours = self.date_time_service.get_time_diff_in_hours( self.start, self.end, 2) return card_hours return 0.0 def get_card_day_of_week_iso(self): return self.date.isoweekday() % 7 @property def validation_issues(self): if (self._validation_issues is None): self._validation_issues = self._validate() return self._validation_issues def is_valid(self): issues = self.validation_issues blocking_issue = next( (issue for issue in issues if issue.level > TimeCardValidationIssue.LEVEL_WARNING), None) return blocking_issue is None def _validate(self): validation_issues = [] card_hours = self.__get_raw_card_hours() # 1. Unclosed timecard (clocked in, but not out) if ((self.start is not None and self.end is None) or (self.in_progress)): validation_issues.append( TimeCardValidationIssue( TimeCardValidationIssue.LEVEL_ERROR, '[Unclosed Card] Clocked in, but not out, by midnight.')) # 2. Negative hours if (card_hours < 0.0): validation_issues.append( TimeCardValidationIssue( TimeCardValidationIssue.LEVEL_ERROR, '[Invalid Card Balance] Card with negative accounted hours.' )) # 3. Long working hours, such as over 10 hours work if (card_hours >= 10.0): validation_issues.append( TimeCardValidationIssue( TimeCardValidationIssue.LEVEL_WARNING, '[Unusual Card Balance] Card with more than 10 hours.')) if (self.system_stopped): validation_issues.append( TimeCardValidationIssue( TimeCardValidationIssue.LEVEL_ERROR, '[System Closed Card] Card stopped accruing hours by system. Please validate!' )) return validation_issues
def __init__(self): self._date_time_service = DateTimeService()
class CompanyPersonnelService(object): def __init__(self): self._date_time_service = DateTimeService() def is_user_employee(self, user_id): try: employee = CompanyUser.objects.get( user=user_id, company_user_type=USER_TYPE_EMPLOYEE) return True except CompanyUser.DoesNotExist: return False def get_company_id_by_employee_user_id(self, employee_user_id): employees = CompanyUser.objects.filter( user=employee_user_id, company_user_type=USER_TYPE_EMPLOYEE) if (len(employees) > 0): return employees[0].company_id return None ''' Get all employees who is not fully terminated during the given time range. i.e. the employee has other employment statuses in the time range than 'Terminated'. This is to serve a common use case of where reports want to include only those employees that are at least partially relate to the company in the view port time range. ''' def get_company_employee_user_ids_non_fully_terminated_in_time_range( self, company_id, time_range_start, time_range_end): all_employee_mappings = self._get_company_employee_user_ids_to_employment_statuses_map( company_id, time_range_start, time_range_end) filtered_user_ids = [] for user_id in all_employee_mappings: for status in all_employee_mappings[user_id]: if (not status == EMPLOYMENT_STATUS_TERMINATED): filtered_user_ids.append(user_id) break return filtered_user_ids ''' Get all employees who are part of the specified employment status as of now This is to serve a common use case of where we want to know which employees are of the specified employment status ''' def get_company_employee_user_ids_currently_with_status( self, company_id, employment_status): return get_company_employee_user_ids_with_status_in_time_range( company_id, employment_status, datetime.date.today(), datetime.date.today()) ''' Get all employees who are part of the specified employment status as of the specified time range. This is to serve a common use case of where we want to know which employees are of the specified employment status within the time range defined ''' def get_company_employee_user_ids_with_status_in_time_range( self, company_id, employment_status, time_range_start, time_range_end): all_employee_mappings = self._get_company_employee_user_ids_to_employment_statuses_map( company_id, time_range_start, time_range_end) employees_with_status = [] for user_id in all_employee_mappings: for status in all_employee_mappings[user_id]: if employment_status == status: employees_with_status.append(user_id) break return employees_with_status ''' Get a mapping, where each pair represents a employee mapped to a list that contains all employement statuses that were at least partially "on" during the given time range. E.g. if an employee was working for the first 2 days in the time range, but got employment terminated since day 3, then this employee would map to a list that contains 2 statuses: Active and Terminated. ''' def _get_company_employee_user_ids_to_employment_statuses_map( self, company_id, time_range_start, time_range_end): result = {} all_employees = CompanyUser.objects.filter( company=company_id, company_user_type=USER_TYPE_EMPLOYEE) for employee in all_employees: # populate with empty values result[employee.user_id] = [] all_employee_profiles = EmployeeProfile.objects.filter( company=company_id) for employee_profile in all_employee_profiles: employee_user_id = employee_profile.person.user.id if (employee_user_id in result): result[ employee_user_id] = self._get_employment_statuses_in_time_range( employee_profile, time_range_start, time_range_end) else: raise Exception( 'Found company employee profile for user that does not relate to the company! Offending user_id is: {}' .format(employee_user_id)) return result def _get_employment_statuses_in_time_range(self, employee_profile, time_range_start, time_range_end): result = [] # [TODO]: We currently don't model a complete trail of # employment status changes, and hence we are # currently only able to infer some information # from employee's start and end employment dates # Conditions # * If employment start date after time range, result => [Prospective] # * If employment end date prior to time range, result => [Terminated] # * If employment start date prior to time range and end date after => [Active] # * If employment start date within time range, result to include/add [Prospective, Active] # * If employment end date within time range, result to include/add [Active, Terminated] if ((employee_profile.start_date or datetime.date.min) > time_range_end): result = [EMPLOYMENT_STATUS_PROSPECTIVE] elif (employee_profile.end_date and employee_profile.end_date < time_range_start): result = [EMPLOYMENT_STATUS_TERMINATED] elif ((employee_profile.start_date or datetime.date.min) <= time_range_start and (not employee_profile.end_date or employee_profile.end_date >= time_range_end)): result = [EMPLOYMENT_STATUS_ACTIVE] if (self._date_time_service.is_time_in_range( employee_profile.start_date or datetime.date.min, time_range_start, time_range_end)): self._ensure_value_in_list(result, EMPLOYMENT_STATUS_PROSPECTIVE) self._ensure_value_in_list(result, EMPLOYMENT_STATUS_ACTIVE) if (employee_profile.end_date and self._date_time_service.is_time_in_range( employee_profile.end_date, time_range_start, time_range_end)): self._ensure_value_in_list(result, EMPLOYMENT_STATUS_ACTIVE) self._ensure_value_in_list(result, EMPLOYMENT_STATUS_TERMINATED) return result def _ensure_value_in_list(self, input_list, value): if (not value): return if (value in input_list): return input_list.append(value) def _get_direct_report_profiles(self, company_id, manager_user_id): if not company_id or not manager_user_id: return [] try: manager_employee_profile = EmployeeProfile.objects.get( person__user=manager_user_id, person__relationship=SELF, company=company_id) except EmployeeProfile.DoesNotExist: return [] direct_report_profiles = EmployeeProfile.objects.filter( manager=manager_employee_profile, employment_status=EMPLOYMENT_STATUS_ACTIVE, company=company_id) return direct_report_profiles ''' Get the list of direct report company_users from the input manager user. Company_id must be provided to make sure the correct company data is queried. ''' def get_direct_report_company_users(self, company_id, manager_user_id): direct_report_profiles = self._get_direct_report_profiles( company_id, manager_user_id) if not direct_report_profiles: return [] direct_report_users = [ profile.person.user for profile in direct_report_profiles ] return CompanyUser.objects.filter(company=company_id, company_user_type=USER_TYPE_EMPLOYEE, user__in=direct_report_users) ''' Get the number of direct reports from the input manager user. Company_id must be provided to make sure the correct company data is queried. ''' def get_direct_report_count(self, company_id, manager_user_id): direct_report_profiles = self._get_direct_report_profiles( company_id, manager_user_id) if not direct_report_profiles: return 0 return direct_report_profiles.count()
class EmployeeDailyPunchCardAggregate(object): date_time_service = DateTimeService() def __init__(self, employee_user_id, date): # List out instance variables self.user_id = employee_user_id self._user_info = None self._user_info_initialized = False self.date = date self._time_punch_cards = [] # Support lasy-evaluated validation self._validation_issues = None @property def user_info(self): if (not self._user_info_initialized): if (self.user_id): user_model = User.objects.get(pk=self.user_id) self._user_info = UserInfo(user_model) self._user_info_initialized = True return self._user_info @property def validation_issues(self): if (self._validation_issues is None): self._validation_issues = self._validate() return self._validation_issues def _validate(self): validation_issues = [] [ validation_issues.extend(card.validation_issues) for card in self._time_punch_cards ] # Aggregate specific validations hours = self.get_total_hours() ## 1.Long working hours in a day, such as over 10 hours work if (hours >= 10.0): validation_issues.append( TimeCardValidationIssue( TimeCardValidationIssue.LEVEL_WARNING, '[Unusual Daily Balance] More than 10 total hours filed in a day.' )) return validation_issues def get_total_hours(self): return sum(card.get_punch_card_hours() for card in self._time_punch_cards) @property def employee_full_name(self): if (self.user_info is None): return None return self.user_info.full_name def add_card(self, time_punch_card): self._time_punch_cards.append(time_punch_card)