def __init__(self): self.integration_provider_service = IntegrationProviderService() self.company_personnel_service = CompanyPersonnelService() self._data_service_registry = {} for integration_provider_type in INTEGRATION_SERVICE_TYPES: self._data_service_registry[integration_provider_type] = {} self._register_data_service_classes()
class _EmployeeDataContext(object): _integration_provider_service = IntegrationProviderService() def __init__(self, employee_user_id, company_id, view_model_factory): self.employee_user_id = employee_user_id self.company_id = company_id self.w4_info = view_model_factory.get_employee_w4_data(employee_user_id) self.state_tax_info = view_model_factory.get_employee_state_tax_data(employee_user_id) self.company_info = view_model_factory.get_company_info() self.person_info = view_model_factory.get_employee_person_info(employee_user_id) self.employee_profile_info = view_model_factory.get_employee_employment_profile_data(employee_user_id) # Employee Number, this comes from the CP system. self.employee_number = self._integration_provider_service.get_employee_integration_provider_external_id( employee_user_id, INTEGRATION_SERVICE_TYPE_PAYROLL, INTEGRATION_PAYROLL_CONNECT_PAYROLL) def user_completed_onboarding(self): if not self.w4_info or not self.w4_info.total_points: return not self.is_new_employee return True def has_complete_data(self): return self.user_completed_onboarding() \ and (self.w4_info or not self.is_new_employee) \ and (self.state_tax_info or not self.is_new_employee) \ and self.company_info \ and self.person_info \ and self.employee_profile_info @property def is_new_employee(self): return self.employee_profile_info and self.employee_profile_info.new_employee
def __init__(self): super(ConnectPayrollDataService, self).__init__() self.view_model_factory = ReportViewModelFactory() self.web_request_service = WebRequestService() self.company_personnel_service = CompanyPersonnelService() self.integration_provider_service = IntegrationProviderService() # Retrieve the api token if available setting_service = SystemSettingsService() self._cp_api_auth_token = setting_service.get_setting_value_by_name( SYSTEM_SETTING_CPAPIAUTHTOKEN) # Also construct the API url, if available self._cp_api_url = None base_uri = setting_service.get_setting_value_by_name( SYSTEM_SETTING_CPAPIBASEURI) employee_route = setting_service.get_setting_value_by_name( SYSTEM_SETTING_CPAPIEMPLOYEEROUTE) if (base_uri and employee_route): self._cp_api_url = base_uri + employee_route
def __init__(self): super(PayrollPeriodExportCsvServiceBase, self).__init__() self.time_punch_card_service = TimePunchCardService() self.integration_provider_service = IntegrationProviderService() self.week_days = 0
class PayrollPeriodExportCsvServiceBase(CsvReportServiceBase): ''' Big assumptions and TODOs * The expectation for this export is weekly * Though salary rate exported here is per pay period * Personal Leave is the only card type that does not count towards hours reported * Company Holiday is counted as 8 hours * Only export employee's who is at least partially active in the period * The report does not try to prorate if employee was terminated during the period. ''' def __init__(self): super(PayrollPeriodExportCsvServiceBase, self).__init__() self.time_punch_card_service = TimePunchCardService() self.integration_provider_service = IntegrationProviderService() self.week_days = 0 ######################################### ## Abstract methods - Begin ######################################### def _get_integration_payroll_service_name(self): raise NotImplementedError() def _needs_write_header_row(self): raise NotImplementedError() def _write_headers(self): raise NotImplementedError() def _get_pay_code(self, earning_type): raise NotImplementedError() def _get_pay_name(self, earning_type): raise NotImplementedError() def _write_row(self, row_data): raise NotImplementedError() def _get_employee_data_rows(self, employee_user_id, person_info, employee_profile_info, employees_reported_hours): raise NotImplementedError() ######################################### ## Abstract methods - End ######################################### def get_report(self, company_id, period_start, period_end, outputStream): client_id = self._get_client_number(company_id) if (not client_id): raise ValueError( 'The company is not properly configured to integrate with Payroll service!' ) if (self._needs_write_header_row()): self._write_headers() self._write_company(company_id, period_start, period_end) self._save(outputStream) def _get_client_number(self, company_id): service_name = self._get_integration_payroll_service_name() return self.integration_provider_service.get_company_integration_provider_external_id( company_id, INTEGRATION_SERVICE_TYPE_PAYROLL, service_name) def _write_company(self, company_id, period_start, period_end): user_ids = self._get_all_employee_user_ids_for_company(company_id) # Get the time tracking data for the company, for the date period # specified employees_reported_hours = self.time_punch_card_service.get_company_users_reported_hours_by_date_range( company_id, period_start, period_end) # Construct the company specific view model factory view_model_factory = CompanyReportViewModelFactory(company_id) company_info = view_model_factory.get_company_info() company_payroll_id = self._get_client_number(company_id) # For each of them, write out his/her information for i in range(len(user_ids)): employee_user_id = user_ids[i] employee_profile_info = view_model_factory.get_employee_employment_profile_data( employee_user_id) # Only report if the employee was, at least partially, active during the # report period. if (employee_profile_info and employee_profile_info. is_employee_active_anytime_in_time_period( period_start, period_end)): person_info = view_model_factory.get_employee_person_info( employee_user_id) self._write_employee(employee_user_id, person_info, company_info, company_payroll_id, employee_profile_info, employees_reported_hours, period_start, period_end) def _write_employee(self, employee_user_id, person_info, company_info, company_payroll_id, employee_profile_info, employees_reported_hours, period_start, period_end): self.week_days = self._get_week_day_number(period_start, period_end, employee_profile_info) rows = self._get_employee_data_rows(employee_user_id, person_info, company_info, company_payroll_id, employee_profile_info, employees_reported_hours) for row in rows: self._write_row(row) def _get_hours_by_earning_type(self, earning_type, employee_hours): if (earning_type == EARNING_TYPE_HOURLY): if employee_hours: return self._normalize_decimal_number( employee_hours.paid_hours) else: return 0.0 elif (earning_type == EARNING_TYPE_SALARY): salary_hours = SALARY_EMPLOYEE_HOURS_PER_DAY * self.week_days if (employee_hours): # if we have PTO, Holiday, unpaid_leave or Sick time cards for salary employees, remove those hours salary_hours -= employee_hours.paid_time_off_hours salary_hours -= employee_hours.holiday_hours salary_hours -= employee_hours.sick_time_hours salary_hours -= employee_hours.unpaid_hours return self._normalize_decimal_number(salary_hours) elif (earning_type == EARNING_TYPE_OVERTIME): return employee_hours.overtime_hours elif (earning_type == EARNING_TYPE_PTO): return employee_hours.paid_time_off_hours elif (earning_type == EARNING_TYPE_SICK_TIME): return employee_hours.sick_time_hours elif (earning_type == EARNING_TYPE_HOLIDAY): return employee_hours.holiday_hours elif (earning_type == EARNING_TYPE_UNPAID_LEAVE): return employee_hours.unpaid_hours def _get_employee_pay_rate(self, employee_profile_info): if (employee_profile_info.pay_type == PAY_TYPE_HOURLY and employee_profile_info.current_hourly_rate): return employee_profile_info.current_hourly_rate return '' def _normalize_decimal_number(self, decimal_number): result = decimal_number if (decimal_number == 0 or decimal_number): result = "{:.2f}".format(float(decimal_number)) return result def _get_week_day_number(self, start, end, profile_info): # The solution below comes from # http://coding.derkeiler.com/Archive/Python/comp.lang.python/2004-09/3758.html dates_start = start if profile_info.hire_date: dates_start = max(start, profile_info.hire_date) dates_end = end if profile_info.end_date: dates_end = min(end, profile_info.end_date) dates = rrule.rruleset() # create an rrule.rruleset instance dates.rrule( rrule.rrule(rrule.DAILY, dtstart=dates_start, until=dates_end)) # this set is INCLUSIVE of alpha and omega dates.exrule( rrule.rrule(rrule.DAILY, byweekday=(rrule.SA, rrule.SU), dtstart=start)) # here's where we exclude the weekend dates return len(list(dates)) # there's probably a faster way to handle this
def __init__(self): self._integration_provider_service = IntegrationProviderService()
class IntegrationProviderDataServiceBase(object): def __init__(self): self._integration_provider_service = IntegrationProviderService() ############################################# ## Abstract members ############################################# def _integration_service_type(self): raise NotImplementedError( 'Concrete implementation needs to specify the integration service type it supports.' ) def _integration_provider_name(self): raise NotImplementedError( 'Concrete implementation needs to specify the name of the service provider it supports.' ) def _internal_sync_employee_data_to_remote(self, employee_user_id): pass def _internal_generate_and_record_external_employee_number( self, employee_user_id): pass ############################################# ## Abstract members ############################################# ''' Whether this given employee is supported by this integration service data facility i.e. whether the corresponding integration service type and provider is available to the given employee ''' def is_supported(self, employee_user_id): return self._integration_provider_service.is_integration_service_available_to_employee( employee_user_id, self._integration_service_type(), self._integration_provider_name()) ''' Synchronize all appropriate data related to the given employee to the external service provider ''' def sync_employee_data_to_remote(self, employee_user_id): if (not self.is_supported(employee_user_id)): raise ValueError( 'The operation is not supported for employee: "{0}"'.format( employee_user_id)) self._internal_sync_employee_data_to_remote(employee_user_id) ''' Generate external employee number (normally for a new employee), and record that in WBM system. ''' def generate_and_record_external_employee_number(self, employee_user_id): if (not self.is_supported(employee_user_id)): raise ValueError( 'The operation is not supported for employee: "{0}"'.format( employee_user_id)) self._internal_generate_and_record_external_employee_number( employee_user_id) def _get_employee_external_id(self, employee_user_id, service_type, provider_name): return self._integration_provider_service.get_employee_integration_provider_external_id( employee_user_id, service_type, provider_name) def _set_employee_external_id(self, employee_user_id, service_type, provider_name, external_id): self._integration_provider_service.set_employee_integration_provider_external_id( employee_user_id, service_type, provider_name, external_id)
def __init__(self): super(AdvantagePayrollCompanySetupCsvService, self).__init__() self.view_model_factory = ReportViewModelFactory() self.integration_provider_service = IntegrationProviderService()
class AdvantagePayrollCompanySetupCsvService(CsvReportServiceBase): def __init__(self): super(AdvantagePayrollCompanySetupCsvService, self).__init__() self.view_model_factory = ReportViewModelFactory() self.integration_provider_service = IntegrationProviderService() def get_report(self, company_id, outputStream): ap_client_id = self._get_ap_client_number(company_id) if (not ap_client_id): raise ValueError( 'The company is not properly configured to integrate with Advantage Payroll service!' ) self._write_headers() self._write_company(company_id, ap_client_id) self._save(outputStream) def _get_ap_client_number(self, company_id): return self.integration_provider_service.get_company_integration_provider_external_id( company_id, INTEGRATION_SERVICE_TYPE_PAYROLL, INTEGRATION_PAYROLL_ADVANTAGE_PAYROLL) def _write_headers(self): # IDs self._write_cell('CLTNO') self._write_cell('EMPNO') # Basic Info self._write_cell('FIRSTNAME') self._write_cell('LASTNAME') self._write_cell('BIRTHDATE') self._write_cell('SEX') self._write_cell('SSN') self._write_cell('ADDR1') self._write_cell('ADDR2') self._write_cell('CITY') self._write_cell('STATE') self._write_cell('ZIPCODE') # Employment Profile self._write_cell('HIREDATE') self._write_cell('CYCLE') self._write_cell('DEPT') self._write_cell('STATUS') self._write_cell('WORKSTATE') # Compensation self._write_cell('SCHEDHRS') self._write_cell('PAYCODE') self._write_cell('HOURLYRATE') self._write_cell('SALARYAMT') self._write_cell('RATEDATE') # Tax Witholding self._write_cell('FITSTATUS') self._write_cell('FITEXEMPT') self._write_cell('FITAMT') self._write_cell('SITSTATUS') self._write_cell('SITCODE') def _write_company(self, company_id, ap_client_id): user_ids = self._get_all_employee_user_ids_for_company(company_id) # For each of them, write out his/her information for i in range(len(user_ids)): self._write_employee(user_ids[i], company_id, ap_client_id) def _user_completed_onboarding(self, company_id, user_id, w4_info): if not w4_info or not w4_info.total_points: comp_user = CompanyUser.objects.get( user=user_id, company_user_type=USER_TYPE_EMPLOYEE, company=company_id) return not comp_user.new_employee return True def _write_employee(self, employee_user_id, company_id, ap_client_id): w4_info = self.view_model_factory.get_employee_w4_data( employee_user_id) if not self._user_completed_onboarding(company_id, employee_user_id, w4_info): return company_info = self.view_model_factory.get_company_info(company_id) person_info = self.view_model_factory.get_employee_person_info( employee_user_id) # now start writing the employee row self._next_row() # Client number, this comes from the AP system self._write_cell(ap_client_id) # Employee Number, this comes from the AP system. ap_employee_number = self.integration_provider_service.get_employee_integration_provider_external_id( employee_user_id, INTEGRATION_SERVICE_TYPE_PAYROLL, INTEGRATION_PAYROLL_ADVANTAGE_PAYROLL) self._write_cell(ap_employee_number) # Now write the basic personal info of the employee self._write_employee_basic_info(person_info) # Now write the employee's employment profile details self._write_employee_employment_profile_info(employee_user_id, company_info) # Noew write the employee's w4/witholding info self._write_employee_w4_info(company_info, w4_info) def _write_employee_basic_info(self, person_info): self._write_cell(person_info.first_name) self._write_cell(person_info.last_name) self._write_cell(self._get_date_string(person_info.birth_date)) self._write_cell(person_info.gender) self._write_cell(person_info.ssn) self._write_cell(person_info.address1) self._write_cell(person_info.address2) self._write_cell(person_info.city) self._write_cell(person_info.state) self._write_cell(person_info.zipcode) def _write_employee_employment_profile_info(self, user_ids, company_info): employee_profile_info = self.view_model_factory.get_employee_employment_profile_data( user_ids, company_info.company_id) if (not employee_profile_info): self._skip_cells(10) return # Profile self._write_cell(self._get_date_string( employee_profile_info.hire_date)) self._write_cell( self._get_pay_cycle_code(employee_profile_info.pay_cycle)) # [TODO]: For now, skip the department info self._skip_cells(1) self._write_cell( self._get_employment_status_code( employee_profile_info.employment_status)) # [TODO]: For now, use the company address state as the employee # work state self._write_cell(company_info.state) # Compensation self._write_cell( self._normalize_decimal_number( employee_profile_info.projected_hours_per_pay_cycle)) self._write_cell( self._get_employee_pay_type_code(employee_profile_info.pay_type)) self._write_cell( self._normalize_decimal_number( employee_profile_info.current_hourly_rate)) self._write_cell( self._get_employee_current_pay_period_salary( employee_profile_info)) self._write_cell( self._get_date_string( employee_profile_info.compensation_effective_date)) def _write_employee_w4_info(self, company_info, w4_info): if (w4_info): status_code = self._get_w4_marriage_status_code( w4_info.marriage_status) self._write_cell(status_code) self._write_cell(w4_info.total_points) self._write_cell(w4_info.extra_amount) # [TODO]: For now, use the federal w4 info to fill state fields # [TODO]: For now, use the company state for the state withold code self._write_cell(status_code) self._write_cell(company_info.state) else: self._skip_cells(5) def _get_w4_marriage_status_code(self, marriage_status): if (marriage_status == W4_MARRIAGE_STATUS_SINGLE): return 'S' elif (marriage_status == W4_MARRIAGE_STATUS_MARRIED): return 'M' elif (marriage_status == W4_MARRIAGE_STATUS_MARRIED_HIGH_SINGLE): return 'S' else: return '' def _get_pay_cycle_code(self, pay_cycle): if (pay_cycle == PERIOD_WEEKLY): return 'W' elif (pay_cycle == PERIOD_BIWEEKLY): return 'B' elif (pay_cycle == PERIOD_SEMIMONTHLY): return 'S' elif (pay_cycle == PERIOD_MONTHLY): return 'M' else: return '' def _get_employment_status_code(self, employment_status): if (employment_status == EMPLOYMENT_STATUS_ACTIVE): return 'A' elif (employment_status == EMPLOYMENT_STATUS_TERMINATED): return 'T' else: return '' def _get_employee_pay_type_code(self, employee_pay_type): if (employee_pay_type == PAY_TYPE_HOURLY): return 'H' elif (employee_pay_type == PAY_TYPE_SALARY): return 'S' else: return '' def _get_employee_current_pay_period_salary(self, employee_profile_info): if (employee_profile_info.pay_type == PAY_TYPE_HOURLY): return 0 else: return self._normalize_decimal_number( employee_profile_info.current_pay_period_salary) def _normalize_decimal_number(self, decimal_number): result = decimal_number if (decimal_number == 0 or decimal_number): result = "{:.2f}".format(float(decimal_number)) return result
def __init__(self): super(ConnectPayrollCompanyEmployeeFrontPageCsvService, self).__init__() self.integration_provider_service = IntegrationProviderService() self.logger = LoggingService() self._state_tax_election_adaptor_factory = ConnectPayrollStateTaxElectionAdaptorFactory()
class ConnectPayrollCompanyEmployeeFrontPageCsvService(CsvReportServiceBase): ######################################################################## ## Items of further investigations ## * [Employement Type] WBM: Only W-2 CP: W-2 and 1099-M ## * [W-4 Status] WBM: Married high rate CP: Head of house hold ## * [W-4 withold state] WBM: Company State (correct?). CP: allow more options ## * [W-4 additional amount] WBM: additional dollar. CP: more options ## * [I-9 status] WBM does not know F-1/J-1 status ## * [Employment Status] Does WBM include terminated employees? ## * [New Hire Flag] Does the NewHire flag stands for the same expectation ## on WBM and CP? ## * [I-9/W-4 non-new employee] WBM does not have data, what would CP expect? ######################################################################## def __init__(self): super(ConnectPayrollCompanyEmployeeFrontPageCsvService, self).__init__() self.integration_provider_service = IntegrationProviderService() self.logger = LoggingService() self._state_tax_election_adaptor_factory = ConnectPayrollStateTaxElectionAdaptorFactory() def get_report(self, company_id, outputStream): try: self.view_model_factory = CompanyReportViewModelFactory(company_id) client_id = self._get_client_number(company_id) if (not client_id): raise ValueError('The company is not properly configured to integrate with Connect Payroll service!') self._write_headers() self._write_company(company_id, client_id) self._save(outputStream) except Exception as e: self.logger.error('Failed to produce Connect Payroll Employee Front Page export for company "{0}"'.format(company_id)) self.logger.error(traceback.format_exc()) raise e def _get_client_number(self, company_id): return self.integration_provider_service.get_company_integration_provider_external_id( company_id, INTEGRATION_SERVICE_TYPE_PAYROLL, INTEGRATION_PAYROLL_CONNECT_PAYROLL) def _write_headers(self): # Integration Identifier self._write_cell('EmpNumber') self._write_cell('Ssn') # Organizational Allocation self._write_cell('HomeLocation') self._write_cell('Division') self._write_cell('Department') # Employee Name self._write_cell('FirstName') self._write_cell('MiddleName') self._write_cell('LastName') # Pay Schedule self._write_cell('PayFrequency') self._write_cell('ScheduleName') # Employee Address self._write_cell('AddressLine1') self._write_cell('AddressLine2') self._write_cell('City') self._write_cell('ResidentState') self._write_cell('ZipCode') self._write_cell('ZipCodeExt') # Employment Type self._write_cell('EmpType') # Federal Tax self._write_cell('FedFS') self._write_cell('FedExemptions') self._write_cell('FedAddl') self._write_cell('FedAmt') self._write_cell('WorkingIn') self._write_cell('WithholdIncomeTaxIn') # State Tax self._write_cell('StateFS') self._write_cell('StateExemptions') self._write_cell('StateAddlExemptions') self._write_cell('StateAddl') self._write_cell('StateAmt') self._write_cell('SecondStateFS') self._write_cell('SecondStateExemptions') self._write_cell('SecondStateAddlExemptions') self._write_cell('SecondStateAddl') self._write_cell('SecondStateAmt') # 1099-M Employee Info # [Remark] WBM Non-supported self._write_cell('ApplyFedId') # Employee HR Info self._write_cell('Email') self._write_cell('BirthDate') # Salary Data self._write_cell('Salary') self._write_cell('SalaryDate') self._write_cell('Rate') self._write_cell('RateDate') # Other HR Data self._write_cell('NewHire') self._write_cell('Seasonal') self._write_cell('HireDate') self._write_cell('Gender') self._write_cell('J1F1Visa') self._write_cell('VisaDate') self._write_cell('Status') self._write_cell('StatusDate') self._write_cell('StatusReason') # Other Tax Data # [Remark] WBM Non-supported self._write_cell('SsExempt') self._write_cell('MedcExempt') self._write_cell('FutaExempt') self._write_cell('FutaExemptReason') self._write_cell('SuiExempt') self._write_cell('NonUsAddress') self._write_cell('PostalCode1') self._write_cell('PostalCode2') self._write_cell('PostalCode3') self._write_cell('Country') def _write_company(self, company_id, client_id): user_ids = self._get_all_employee_user_ids_for_company(company_id) # For each of them, write out his/her information for i in range(len(user_ids)): self._write_employee(user_ids[i], company_id, client_id) def _write_employee(self, employee_user_id, company_id, client_id): employee_data_context = _EmployeeDataContext(employee_user_id, company_id, self.view_model_factory) if not employee_data_context.user_completed_onboarding(): self.logger.warn('Skipping employee "{0}": Onboarding not complete.'.format(employee_user_id)) return if (not employee_data_context.has_complete_data()): self.logger.warn('Skipping employee "{0}": Missing necessary information'.format(employee_user_id)) return # now start writing the employee row self._next_row() self._write_integration_info(employee_data_context) self._write_organizational_allocation_info(employee_data_context) self._write_employee_name(employee_data_context) self._write_employee_pay_schedule(employee_data_context) self._write_employee_address(employee_data_context) self._write_employment_type(employee_data_context) self._write_federal_tax_info(employee_data_context) self._write_state_tax_info(employee_data_context) self._write_1099M_data(employee_data_context) self._write_employee_HR_info(employee_data_context) self._write_employee_salary_data(employee_data_context) self._write_employee_other_HR_data(employee_data_context) def _write_integration_info(self, employee_data_context): self._write_cell(employee_data_context.employee_number) self._write_cell(employee_data_context.person_info.ssn) def _write_organizational_allocation_info(self, employee_data_context): # Skippng 'HomeLocation' as it is not used self._skip_cells(1) employee_profile_info = employee_data_context.employee_profile_info if (employee_profile_info.division and employee_profile_info.division.code): self._write_cell(employee_profile_info.division.code) else: self._skip_cells(1) if (employee_profile_info.department and employee_profile_info.department.code): self._write_cell(employee_profile_info.department.code) else: self._skip_cells(1) def _write_employee_name(self, employee_data_context): person_info = employee_data_context.person_info self._write_cell(person_info.first_name) self._write_cell(person_info.middle_name) self._write_cell(person_info.last_name) def _write_employee_pay_schedule(self, employee_data_context): employee_profile_info = employee_data_context.employee_profile_info mapped_paycode = self._get_pay_cycle_code(employee_profile_info.pay_cycle) if (not mapped_paycode): self.logger.warn('[Data Issue] Employee "{0}": has invalid pay-code'.format(employee_data_context.employee_user_id)) self._skip_cells(1) else: self._write_cell(mapped_paycode) # Skip the 'ScheduleName' field self._skip_cells(1) def _get_pay_cycle_code(self, pay_cycle): if (pay_cycle == PERIOD_WEEKLY): return 'Weekly' elif(pay_cycle == PERIOD_BIWEEKLY): return 'Biweekly' elif(pay_cycle == PERIOD_SEMIMONTHLY): return 'Semi-Monthly' elif(pay_cycle == PERIOD_MONTHLY): return 'Monthly' else: return '' def _write_employee_address(self, employee_data_context): person_info = employee_data_context.person_info self._write_cell(person_info.address1) self._write_cell(person_info.address2) self._write_cell(person_info.city) self._write_cell(person_info.state) zip_and_ext = person_info.get_zipcode_and_extension() self._write_cell(zip_and_ext[0]) self._write_cell(zip_and_ext[1]) def _write_employment_type(self, employee_data_context): # [Remark]: We only support W2 employee (?) self._write_cell('W2') def _write_federal_tax_info(self, employee_data_context): w4_info = employee_data_context.w4_info if (not w4_info): self._skip_cells(6) else: company_info = employee_data_context.company_info status_code = self._get_w4_marriage_status_code(w4_info.marriage_status) self._write_cell(status_code) self._write_cell(w4_info.total_points) self._write_cell('A') self._write_cell(w4_info.extra_amount) self._write_cell(company_info.state) self._write_cell(company_info.state) def _get_w4_marriage_status_code(self, marriage_status): if (marriage_status == W4_MARRIAGE_STATUS_SINGLE): return 'S' elif(marriage_status == W4_MARRIAGE_STATUS_MARRIED): return 'M' elif(marriage_status == W4_MARRIAGE_STATUS_MARRIED_HIGH_SINGLE): return 'H' else: return '' def _write_state_tax_info(self, employee_data_context): state_tax_info = employee_data_context.state_tax_info # This limits how many state elections CP can take and hence # we can output export_limit = 2 counter = 0 if (state_tax_info): all_state_elections = state_tax_info.get_all_state_elections() for state in all_state_elections: if (counter >= export_limit): break adaptor = self._state_tax_election_adaptor_factory.get_adaptor(state, all_state_elections[state]) if (adaptor): self._write_cell(adaptor.get_filing_status()) self._write_cell(adaptor.get_total_exemptions()) self._write_cell(adaptor.get_additional_exemptions()) self._write_cell(adaptor.get_additional_amount_code()) self._write_cell(adaptor.get_additional_amount()) counter = counter + 1 # Skip any set of fields that the employee does not have data to fill # E.g. secondary state witholding self._skip_cells((export_limit - counter) * 5) def _write_1099M_data(self, employee_data_context): # [Remark]: WBM does not support 1099-M self._skip_cells(1) def _write_employee_HR_info(self, employee_data_context): person_info = employee_data_context.person_info self._write_cell(person_info.email) self._write_cell(self._get_date_string(person_info.birth_date)) def _write_employee_salary_data(self, employee_data_context): employee_profile_info = employee_data_context.employee_profile_info pay_type = employee_profile_info.pay_type if (pay_type == PAY_TYPE_HOURLY): self._skip_cells(2) self._write_cell(self._normalize_decimal_number(employee_profile_info.current_hourly_rate)) self._write_cell(self._get_date_string(employee_profile_info.compensation_effective_date)) elif (pay_type == PAY_TYPE_SALARY): self._write_cell(self._normalize_decimal_number(employee_profile_info.annual_salary)) self._write_cell(self._get_date_string(employee_profile_info.compensation_effective_date)) self._skip_cells(2) else: self.logger.warn('Skipping data for employee "{0}": Invalid pay type.'.format(employee_data_context.employee_user_id)) self._skip_cells(4) def _write_employee_other_HR_data(self, employee_data_context): employee_profile_info = employee_data_context.employee_profile_info person_info = employee_data_context.person_info self._write_cell(employee_profile_info.new_employee) # [Remark]: Seasonal is not supported on WBM and seems to not be used by CP self._skip_cells(1) self._write_cell(self._get_date_string(employee_profile_info.hire_date)) self._write_cell(person_info.gender) # [Remark]: Skip all J1/F1 Visa self._skip_cells(2) code_and_date = self._get_employement_status_code_and_date(employee_profile_info) if (not code_and_date): self.logger.warn('Skipping data for employee "{0}": Invalid employment status'.format(employee_data_context.employee_user_id)) self._skip_cells(2) else: self._write_cell(code_and_date['code']) self._write_cell(code_and_date['date']) # [Remark]: Skip the status reason self._skip_cells(1) def _get_employement_status_code_and_date(self, employee_profile_info): employment_status = employee_profile_info.employment_status if (employment_status == EMPLOYMENT_STATUS_ACTIVE): return { 'code': 'Active', 'date': self._get_date_string(employee_profile_info.hire_date) } elif (employment_status == EMPLOYMENT_STATUS_TERMINATED): return { 'code': 'Terminated', 'date': self._get_date_string(employee_profile_info.end_date) } else: return None def _write_other_tax_data(self, employee_data_context): # [Remark] WBM Non-supported # [Remark] CP does not seem to use self._skip_cells(10) def _normalize_decimal_number(self, decimal_number): result = decimal_number if (decimal_number == 0 or decimal_number): result = "{:.2f}".format(float(decimal_number)) return result
def __init__(self): super(AdvantagePayrollDataService, self).__init__() self.company_personnel_service = CompanyPersonnelService() self.integration_provider_service = IntegrationProviderService()
class AdvantagePayrollDataService(IntegrationProviderDataServiceBase): def __init__(self): super(AdvantagePayrollDataService, self).__init__() self.company_personnel_service = CompanyPersonnelService() self.integration_provider_service = IntegrationProviderService() def _integration_service_type(self): return INTEGRATION_SERVICE_TYPE_PAYROLL def _integration_provider_name(self): return INTEGRATION_PAYROLL_ADVANTAGE_PAYROLL def _internal_generate_and_record_external_employee_number(self, employee_user_id): # First check whether the said employee already have a number # If so, this is an exception state, log it, and skip the operation employee_number = self.integration_provider_service.get_employee_integration_provider_external_id( employee_user_id, self._integration_service_type(), self._integration_provider_name()) if (employee_number): logging.error('Invalid Operation: Try to generate external ID for employee (User ID={0}) already has one!'.format(employee_user_id)) return company_id = self.company_personnel_service.get_company_id_by_employee_user_id(employee_user_id) next_employee_number = self._get_next_external_employee_number(company_id) # Now save the next usable external employee number to the profile # of the specified employee self._set_employee_external_id( employee_user_id, self._integration_service_type(), self._integration_provider_name(), next_employee_number ) def _get_next_external_employee_number(self, company_id): employee_number_seed_str = self.integration_provider_service.get_company_integration_provider_employee_external_id_seed( company_id, self._integration_service_type(), self._integration_provider_name()) employee_number_seed = 0 if (employee_number_seed_str): try: employee_number_seed = int(employee_number_seed_str) except ValueError: logging.exception('Encountered malformed external employee number seed: "{0}"'.format(employee_number_seed_str)) max_employee_number = self._get_max_external_employee_number(company_id) return max(employee_number_seed, max_employee_number) + 1 def _get_max_external_employee_number(self, company_id): all_employee_data = CompanyUserIntegrationProvider.objects.filter( company_user__company=company_id, integration_provider__service_type=self._integration_service_type(), integration_provider__name=self._integration_provider_name()) employee_numbers = [] for employee_data in all_employee_data: if (employee_data.company_user_external_id): # For AP, it is assumed that all employee numbers # are positive integers. Though just build some # fault tolerence here, to log the exception and # let it go, to not block all employee creations # if for some reason there is a malformed ID in # system. try: employee_number = int(employee_data.company_user_external_id) employee_numbers.append(employee_number) except ValueError: logging.exception('Encountered malformed external employee number: "{0}"'.format(employee_data.company_user_external_id)) if (len(employee_numbers) > 0): return max(employee_numbers) return 0
class CompanyIntegrationProviderDataService(object): _logger = LoggingService() def __init__(self): self.integration_provider_service = IntegrationProviderService() self.company_personnel_service = CompanyPersonnelService() self._data_service_registry = {} for integration_provider_type in INTEGRATION_SERVICE_TYPES: self._data_service_registry[integration_provider_type] = {} self._register_data_service_classes() def _register_data_service_classes(self): # Register all data services self._data_service_registry[INTEGRATION_SERVICE_TYPE_PAYROLL][INTEGRATION_PAYROLL_CONNECT_PAYROLL] = ConnectPayrollDataService self._data_service_registry[INTEGRATION_SERVICE_TYPE_PAYROLL][INTEGRATION_PAYROLL_ADVANTAGE_PAYROLL] = AdvantagePayrollDataService def sync_employee_data_to_remote(self, employee_user_id): # Enumerate through all the integration providers associated # with the company, identify all registered data services, # and invoke them to sync with the remote company_id = self.company_personnel_service.get_company_id_by_employee_user_id(employee_user_id) if (not company_id): return return self._enumerate_company_data_services(company_id, lambda data_service: data_service.sync_employee_data_to_remote(employee_user_id)) def generate_and_record_external_employee_number(self, employee_user_id): company_id = self.company_personnel_service.get_company_id_by_employee_user_id(employee_user_id) if (not company_id): raise ValueError('The given employee user ID "{0}" is not properly linked to a valid company.'.format(employee_user_id)) return self._enumerate_company_data_services(company_id, lambda data_service: data_service.generate_and_record_external_employee_number(employee_user_id)) def _enumerate_company_data_services(self, company_id, data_service_action): # Record failed actions and report back to caller failed_data_service_records = [] company_integration_providers = self.integration_provider_service.get_company_integration_providers(company_id) for service_type in company_integration_providers: company_service_type_provider = company_integration_providers[service_type] if (company_service_type_provider): provider_name = company_service_type_provider['integration_provider']['name'] # Now we have the service type and the provider name, check whether # we have a registered data service class if (service_type in self._data_service_registry): service_type_data_services = self._data_service_registry[service_type] if (provider_name in service_type_data_services): # create an instance of the date service, and invoke the action data_service = service_type_data_services[provider_name]() # Collect the current data service specs into a record instance # for logging and anormaly reporting to upstream service_action_record = IntegrationDataServiceActionRecord( company_id=company_id, service_type=service_type, provider_name=provider_name, service_action=data_service_action ) try: data_service_action(data_service) self._logger.error('Successfully executed integration data action') self._logger.info(service_action_record) except Exception as e: self._logger.error('Failed to complete integration data action') self._logger.info(service_action_record) failed_data_service_records.append(service_action_record) else: self._logger.warning('Unsupported integration service type encoutered: "{0}"'.format(service_type)) return failed_data_service_records
class ConnectPayrollDataService(IntegrationProviderDataServiceBase): _logger = LoggingService() def __init__(self): super(ConnectPayrollDataService, self).__init__() self.view_model_factory = ReportViewModelFactory() self.web_request_service = WebRequestService() self.company_personnel_service = CompanyPersonnelService() self.integration_provider_service = IntegrationProviderService() # Retrieve the api token if available setting_service = SystemSettingsService() self._cp_api_auth_token = setting_service.get_setting_value_by_name( SYSTEM_SETTING_CPAPIAUTHTOKEN) # Also construct the API url, if available self._cp_api_url = None base_uri = setting_service.get_setting_value_by_name( SYSTEM_SETTING_CPAPIBASEURI) employee_route = setting_service.get_setting_value_by_name( SYSTEM_SETTING_CPAPIEMPLOYEEROUTE) if (base_uri and employee_route): self._cp_api_url = base_uri + employee_route def _integration_service_type(self): return INTEGRATION_SERVICE_TYPE_PAYROLL def _integration_provider_name(self): return INTEGRATION_PAYROLL_CONNECT_PAYROLL def _internal_generate_and_record_external_employee_number( self, employee_user_id): # First check whether the said employee already have a number # If so, this is an exception state, log it, and skip the operation employee_number = self.integration_provider_service.get_employee_integration_provider_external_id( employee_user_id, self._integration_service_type(), self._integration_provider_name()) if (employee_number): logging.error( 'Invalid Operation: Try to generate external ID for employee (User ID={0}) already has one!' .format(employee_user_id)) return company_id = self.company_personnel_service.get_company_id_by_employee_user_id( employee_user_id) next_employee_number = self._get_next_external_employee_number( company_id) # Now save the next usable external employee number to the profile # of the specified employee self._set_employee_external_id(employee_user_id, self._integration_service_type(), self._integration_provider_name(), next_employee_number) def _get_next_external_employee_number(self, company_id): return 0 def _internal_sync_employee_data_to_remote(self, employee_user_id): # If the Connect Payroll API's auth token is not specified # in the environment, consider this feature to be off, and # skip all together. if (not self._cp_api_auth_token): return if (not self._cp_api_url): return # Also check whether the employee belong to a company with # the right setup with the remote system. And also skip if # this is not the case external_company_id = self._get_cp_client_code_by_employee( employee_user_id) if (not external_company_id): return try: # Populate the data object from the current state of the employee in WBM system # Also apply client(WBM) side validation on the data, based on understanding of # documentation from ConnectPay employee_data_dto = self._get_employee_data_dto( employee_user_id, external_company_id) issue_list = self._validate_employee_data_dto(employee_data_dto) if (issue_list and len(issue_list) > 0): raise RuntimeError( 'There are problems collecting complete data required to sync to ConnectPay API for employee "{0}"' .format(employee_user_id), issue_list) if (employee_data_dto.payrollId): # Already exists in CP system, update self._logger.info('Updating Employee CP ID: ' + employee_data_dto.payrollId) self._logger.info(employee_data_dto) self._update_employee_data_to_remote(employee_data_dto) else: # Does not yet exist in CP system, new employee addition, create self._logger.info( 'Creating new employee record on CP system ...') self._logger.info(employee_data_dto) payroll_id = self._create_employee_data_to_remote( employee_data_dto) self._logger.info( 'Created Employee CP ID: {0}'.format(payroll_id)) # Sync the cp ID from the response self._set_employee_external_id( employee_user_id, self._integration_service_type(), self._integration_provider_name(), payroll_id) except Exception as e: self._logger.error(traceback.format_exc()) raise def _get_employee_data_dto(self, employee_user_id, external_company_id): # First populate the CP identifiers dto = ConnectPayrollEmployeeDto() dto.payrollId = self._get_employee_external_id( employee_user_id, self._integration_service_type(), self._integration_provider_name()) dto.companyId = external_company_id # Now populate other data company_id = self.company_personnel_service.get_company_id_by_employee_user_id( employee_user_id) company_info = self.view_model_factory.get_company_info(company_id) person_info = self.view_model_factory.get_employee_person_info( employee_user_id) # Employee basic data dto.ssn = person_info.ssn dto.firstName = person_info.first_name dto.lastName = person_info.last_name dto.dob = self._get_date_string(person_info.birth_date) dto.gender = person_info.gender dto.address1 = person_info.address1 dto.address2 = person_info.address2 dto.city = person_info.city dto.country = person_info.country dto.state = person_info.state dto.zip = person_info.zipcode dto.email = person_info.email if (len(person_info.phones) > 0): dto.phone = person_info.phones[0]['number'] # Employment data employee_profile_info = self.view_model_factory.get_employee_employment_profile_data( employee_user_id, company_info.company_id) if (employee_profile_info): dto.jobTitle = employee_profile_info.job_title dto.fullTime = employee_profile_info.is_full_time_employee() dto.hireDate = self._get_date_string( employee_profile_info.hire_date) dto.originalHireDate = self._get_date_string( employee_profile_info.hire_date) # [TODO]: Needs specification on employee status values dto.employeeStatus = '3' dto.terminationDate = self._get_date_string( employee_profile_info.end_date) # Salary data dto.payEffectiveDate = self._get_date_string( employee_profile_info.compensation_effective_date) dto.annualBaseSalary = self._get_decimal_string( employee_profile_info.annual_salary) dto.baseHourlyRate = self._get_decimal_string( employee_profile_info.current_hourly_rate) dto.hoursPerWeek = self._get_decimal_string( employee_profile_info.projected_hours_per_week) # Other employee_i9_info = self.view_model_factory.get_employee_i9_data( employee_user_id) if (employee_i9_info): self.usCitizen = employee_i9_info.citizen_data is not None return dto def _validate_employee_data_dto(self, employee_data_dto): issue_list = [] # System Data _DataValidator(employee_data_dto, 'companyId', issue_list) \ .with_value_exists_check() \ .with_value_length_check(6, 6) \ .validate() _DataValidator(employee_data_dto, 'payrollId', issue_list) \ .with_value_valid_integer_check() \ .validate() # Employee bio data and basic info _DataValidator(employee_data_dto, 'ssn', issue_list) \ .with_value_exists_check() \ .with_value_length_check(9, 9) \ .validate() _DataValidator(employee_data_dto, 'firstName', issue_list) \ .with_value_exists_check() \ .with_value_length_check(1, 20) \ .validate() _DataValidator(employee_data_dto, 'middleName', issue_list) \ .with_value_length_check(0, 20) \ .validate() _DataValidator(employee_data_dto, 'lastName', issue_list) \ .with_value_exists_check() \ .with_value_length_check(1, 20) \ .validate() _DataValidator(employee_data_dto, 'dob', issue_list) \ .with_value_exists_check() \ .with_value_valid_datetime_check() \ .validate() _DataValidator(employee_data_dto, 'gender', issue_list) \ .with_value_exists_check() \ .with_value_in_list_check(['M', 'F']) \ .validate() _DataValidator(employee_data_dto, 'address1', issue_list) \ .with_value_length_check(0, 30) \ .validate() _DataValidator(employee_data_dto, 'address2', issue_list) \ .with_value_length_check(0, 30) \ .validate() _DataValidator(employee_data_dto, 'city', issue_list) \ .with_value_length_check(0, 28) \ .validate() _DataValidator(employee_data_dto, 'state', issue_list) \ .with_value_exists_check() \ .with_value_length_check(2, 2) \ .validate() _DataValidator(employee_data_dto, 'zip', issue_list) \ .with_value_exists_check() \ .with_value_length_check(5, 10) \ .validate() _DataValidator(employee_data_dto, 'country', issue_list) \ .with_value_length_check(0, 30) \ .validate() _DataValidator(employee_data_dto, 'email', issue_list) \ .with_value_exists_check() \ .with_value_length_check(1, 250) \ .validate() _DataValidator(employee_data_dto, 'phone', issue_list) \ .with_value_length_check(0, 14) \ .validate() _DataValidator(employee_data_dto, 'phone', issue_list) \ .with_value_length_check(0, 14) \ .validate() # Employment data _DataValidator(employee_data_dto, 'department', issue_list) \ .with_value_valid_integer_check() \ .validate() _DataValidator(employee_data_dto, 'division', issue_list) \ .with_value_valid_integer_check() \ .validate() _DataValidator(employee_data_dto, 'union', issue_list) \ .with_value_type_boolean_check() \ .validate() _DataValidator(employee_data_dto, 'jobTitle', issue_list) \ .with_value_length_check(0, 30) \ .validate() _DataValidator(employee_data_dto, 'fullTime', issue_list) \ .with_value_type_boolean_check() \ .validate() _DataValidator(employee_data_dto, 'seasonal', issue_list) \ .with_value_type_boolean_check() \ .validate() _DataValidator(employee_data_dto, 'hireDate', issue_list) \ .with_value_exists_check() \ .with_value_valid_datetime_check() \ .validate() _DataValidator(employee_data_dto, 'originalHireDate', issue_list) \ .with_value_valid_datetime_check() \ .validate() _DataValidator(employee_data_dto, 'terminationDate', issue_list) \ .with_value_valid_datetime_check() \ .validate() self.employeeStatus = None # Salary data _DataValidator(employee_data_dto, 'payEffectiveDate', issue_list) \ .with_value_valid_datetime_check() \ .validate() _DataValidator(employee_data_dto, 'annualBaseSalary', issue_list) \ .with_value_valid_decimal_check() \ .validate() _DataValidator(employee_data_dto, 'baseHourlyRate', issue_list) \ .with_value_valid_decimal_check() \ .validate() return issue_list def _update_employee_data_to_remote(self, employee_data_dto): data = employee_data_dto.__dict__ response = self.web_request_service.put( self._cp_api_url, data_object=data, auth_token=self._cp_api_auth_token) response.raise_for_status() def _create_employee_data_to_remote(self, employee_data_dto): data = employee_data_dto.__dict__ response = self.web_request_service.post( self._cp_api_url, data_object=data, auth_token=self._cp_api_auth_token) response.raise_for_status() # Also, we really only expect here the below based on CP API behavior # * HTTP 200 # * body contains the resultant ID created # So throw if we receive anything else if (response.status_code != 200): raise RuntimeError( 'POST to ConnectPay Employee API resulted in a non-200 status: "{0}"' .format(response.status_code), response) if (not response.text): raise RuntimeError( 'POST to ConnectPay Employee API resulted in empty body, and hence was not able to receive new employee ID.' ) return response.text def _get_cp_client_code_by_employee(self, employee_user_id): company_id = self.company_personnel_service.get_company_id_by_employee_user_id( employee_user_id) if (company_id): return self.integration_provider_service.get_company_integration_provider_external_id( company_id, self._integration_service_type(), self._integration_provider_name()) return None def _get_date_string(self, date): if date: try: return date.isoformat() except: return None else: return None def _get_decimal_string(self, input_value): if isinstance(input_value, decimal.Decimal): return str(input_value) return input_value
def get(self, request, company_id, format=None): service = IntegrationProviderService() result = service.get_company_integration_providers(company_id) return Response(result)