brynq-sdk-alight 1.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brynq_sdk_alight/__init__.py +1058 -0
- brynq_sdk_alight/address.py +72 -0
- brynq_sdk_alight/archive/flat_wrapper.py +139 -0
- brynq_sdk_alight/archive/hrxml_generator.py +280 -0
- brynq_sdk_alight/archive/managers.py +132 -0
- brynq_sdk_alight/archive/managers_generic.py +114 -0
- brynq_sdk_alight/archive/managers_old_complex.py +294 -0
- brynq_sdk_alight/archive/managers_simple.py +229 -0
- brynq_sdk_alight/employee.py +81 -0
- brynq_sdk_alight/job.py +89 -0
- brynq_sdk_alight/leave.py +97 -0
- brynq_sdk_alight/pay_elements.py +97 -0
- brynq_sdk_alight/salary.py +89 -0
- brynq_sdk_alight/schemas/__init__.py +26 -0
- brynq_sdk_alight/schemas/absence.py +83 -0
- brynq_sdk_alight/schemas/address.py +113 -0
- brynq_sdk_alight/schemas/employee.py +656 -0
- brynq_sdk_alight/schemas/generated_envelope_xsd_schema/__init__.py +38683 -0
- brynq_sdk_alight/schemas/generated_envelope_xsd_schema/process_pay_serv_emp.py +622264 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/__init__.py +10965 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/csec_person.py +39808 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/hrxml_indicative_data.py +90318 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_bod.py +33869 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_currency_code_iso_7_04.py +365 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_language_code_iso_7_04.py +16 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_mimemedia_type_code_iana_7_04.py +16 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_unit_code_unece_7_04.py +14 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_lists.py +535 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_qualified_data_types.py +84 -0
- brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_unqualified_data_types.py +1449 -0
- brynq_sdk_alight/schemas/job.py +145 -0
- brynq_sdk_alight/schemas/leave.py +58 -0
- brynq_sdk_alight/schemas/payments.py +207 -0
- brynq_sdk_alight/schemas/salary.py +67 -0
- brynq_sdk_alight/schemas/termination.py +48 -0
- brynq_sdk_alight/schemas/timequota.py +66 -0
- brynq_sdk_alight/schemas/utils.py +452 -0
- brynq_sdk_alight/termination.py +103 -0
- brynq_sdk_alight/time_elements.py +121 -0
- brynq_sdk_alight/time_quotas.py +114 -0
- brynq_sdk_alight-1.0.0.dev0.dist-info/METADATA +20 -0
- brynq_sdk_alight-1.0.0.dev0.dist-info/RECORD +44 -0
- brynq_sdk_alight-1.0.0.dev0.dist-info/WHEEL +5 -0
- brynq_sdk_alight-1.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fully generic manager for the Alight SDK.
|
|
3
|
+
Pure XSD schema-driven transformations - zero hardcoded mappings.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Optional, TYPE_CHECKING, List, get_origin, get_args
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from .schemas.hrxml_indicative_data import IndicativeDataType
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from . import Alight
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EmployeeManager:
|
|
16
|
+
"""Fully generic employee manager - pure XSD schema-driven conversion."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, alight_sdk: 'Alight'):
|
|
19
|
+
self.alight_sdk = alight_sdk
|
|
20
|
+
|
|
21
|
+
def create_newhire_simple(self, flat_data: Dict[str, Any],
|
|
22
|
+
logical_id: Optional[str] = None,
|
|
23
|
+
pretty_print: bool = True,
|
|
24
|
+
save_to_file: bool = False,
|
|
25
|
+
filename: Optional[str] = None) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Create NewHire XML from flat dictionary using pure XSD schema conversion.
|
|
28
|
+
"""
|
|
29
|
+
nested_data = self._flat_to_nested(flat_data)
|
|
30
|
+
return self.alight_sdk.generate_newhire_xml(
|
|
31
|
+
person_data=nested_data,
|
|
32
|
+
logical_id=logical_id,
|
|
33
|
+
pretty_print=pretty_print
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def create_employee_change_simple(self, flat_data: Dict[str, Any],
|
|
37
|
+
logical_id: Optional[str] = None,
|
|
38
|
+
pretty_print: bool = True,
|
|
39
|
+
save_to_file: bool = False,
|
|
40
|
+
filename: Optional[str] = None) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Create Employee Change XML from flat dictionary using pure XSD schema conversion.
|
|
43
|
+
"""
|
|
44
|
+
nested_data = self._flat_to_nested(flat_data)
|
|
45
|
+
return self.alight_sdk.generate_employee_change_xml(
|
|
46
|
+
person_data=nested_data,
|
|
47
|
+
logical_id=logical_id,
|
|
48
|
+
pretty_print=pretty_print
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def _flat_to_nested(self, flat_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Convert flat data to nested using PURE XSD schema inspection.
|
|
54
|
+
Zero hardcoded mappings - everything is generic and schema-driven.
|
|
55
|
+
"""
|
|
56
|
+
# Use the generic XSD-aware conversion directly on the main schema
|
|
57
|
+
return self._xsd_flat_to_nested(flat_data, IndicativeDataType)
|
|
58
|
+
|
|
59
|
+
def _xsd_flat_to_nested(self, flat_dict: Dict[str, Any], model: BaseModel) -> Dict[str, Any]:
|
|
60
|
+
"""
|
|
61
|
+
Generic XSD-aware version exactly like your Functions.flat_dict_to_nested_dict.
|
|
62
|
+
Recursively processes the schema and handles XSD {"value": ...} format automatically.
|
|
63
|
+
"""
|
|
64
|
+
nested = {}
|
|
65
|
+
|
|
66
|
+
for name, field in model.model_fields.items():
|
|
67
|
+
key_in_input = name # Original model field name as key in flat_dict
|
|
68
|
+
alias = field.alias or name
|
|
69
|
+
|
|
70
|
+
# Handle nested BaseModel fields (recursively) - exact same logic as your function
|
|
71
|
+
if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
|
|
72
|
+
nested[alias] = self._xsd_flat_to_nested(flat_dict, field.annotation)
|
|
73
|
+
|
|
74
|
+
# Handle Union types with BaseModel (like Optional[SomeModel]) - exact same logic
|
|
75
|
+
elif any(isinstance(item, type) and issubclass(item, BaseModel) for item in get_args(field.annotation)):
|
|
76
|
+
# Get the BaseModel class from the Union
|
|
77
|
+
nested_model = next(item for item in get_args(field.annotation)
|
|
78
|
+
if isinstance(item, type) and issubclass(item, BaseModel))
|
|
79
|
+
nested[alias] = self._xsd_flat_to_nested(flat_dict, nested_model)
|
|
80
|
+
|
|
81
|
+
# Handle primitive fields - your logic + XSD value wrapping
|
|
82
|
+
else:
|
|
83
|
+
if key_in_input in flat_dict:
|
|
84
|
+
value = flat_dict[key_in_input]
|
|
85
|
+
|
|
86
|
+
# XSD enhancement: Check if this field type needs {"value": ...} wrapping
|
|
87
|
+
field_type = self._unwrap_optional_and_list(field.annotation)
|
|
88
|
+
if (hasattr(field_type, 'model_fields') and
|
|
89
|
+
'value' in getattr(field_type, 'model_fields', {})):
|
|
90
|
+
nested[alias] = {"value": value}
|
|
91
|
+
else:
|
|
92
|
+
nested[alias] = value
|
|
93
|
+
|
|
94
|
+
return nested
|
|
95
|
+
|
|
96
|
+
def _unwrap_optional_and_list(self, field_type):
|
|
97
|
+
"""Helper to unwrap Optional and List types to get the core type."""
|
|
98
|
+
# Handle Optional[T] (Union[T, None])
|
|
99
|
+
origin = get_origin(field_type)
|
|
100
|
+
if origin is type(Optional[str]) or str(origin) == 'typing.Union':
|
|
101
|
+
args = get_args(field_type)
|
|
102
|
+
if args:
|
|
103
|
+
for arg in args:
|
|
104
|
+
if arg is not type(None):
|
|
105
|
+
field_type = arg
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
# Handle List[T]
|
|
109
|
+
if get_origin(field_type) is list:
|
|
110
|
+
args = get_args(field_type)
|
|
111
|
+
if args:
|
|
112
|
+
field_type = args[0]
|
|
113
|
+
|
|
114
|
+
return field_type
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simplified manager for the Alight SDK.
|
|
3
|
+
Pure dictionary transformations - no unnecessary classes or complexity.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Optional, TYPE_CHECKING, List, get_origin, get_args
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from .schemas.hrxml_indicative_data import IndicativeDataType, IndicativePerson
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from . import Alight
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EmployeeManager:
|
|
16
|
+
"""Simple employee manager - just flat-to-nested dictionary conversion."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, alight_sdk: 'Alight'):
|
|
19
|
+
self.alight_sdk = alight_sdk
|
|
20
|
+
|
|
21
|
+
# Simple field categorization
|
|
22
|
+
self.field_categories = {
|
|
23
|
+
'extension': ['iban', 'bic', 'account_holder', 'notes'],
|
|
24
|
+
'salary': ['base_salary', 'bonus_amount', 'salary_currency', 'pay_element_code'],
|
|
25
|
+
'job': ['job_title', 'weekly_hours', 'country'],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def create_newhire_simple(self, flat_data: Dict[str, Any],
|
|
29
|
+
logical_id: Optional[str] = None,
|
|
30
|
+
pretty_print: bool = True,
|
|
31
|
+
save_to_file: bool = False,
|
|
32
|
+
filename: Optional[str] = None) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Create NewHire XML from flat dictionary - that's it!
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
flat_data: Simple flat dictionary with snake_case keys
|
|
38
|
+
logical_id: Optional logical ID
|
|
39
|
+
pretty_print: Format XML nicely
|
|
40
|
+
save_to_file: Save to file
|
|
41
|
+
filename: Optional filename
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
str: HR-XML ready for Alight
|
|
45
|
+
"""
|
|
46
|
+
nested_data = self._flat_to_nested(flat_data)
|
|
47
|
+
return self.alight_sdk.generate_newhire_xml(
|
|
48
|
+
person_data=nested_data.get('person_data'),
|
|
49
|
+
extension_data=nested_data.get('extension_data'),
|
|
50
|
+
pay_elements_data=nested_data.get('salary_data'),
|
|
51
|
+
logical_id=logical_id,
|
|
52
|
+
pretty_print=pretty_print
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def create_employee_change_simple(self, flat_data: Dict[str, Any],
|
|
56
|
+
logical_id: Optional[str] = None,
|
|
57
|
+
pretty_print: bool = True,
|
|
58
|
+
save_to_file: bool = False,
|
|
59
|
+
filename: Optional[str] = None) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Create Employee Change XML from flat dictionary - that's it!
|
|
62
|
+
"""
|
|
63
|
+
nested_data = self._flat_to_nested(flat_data)
|
|
64
|
+
return self.alight_sdk.generate_employee_change_xml(
|
|
65
|
+
person_data=nested_data.get('person_data'),
|
|
66
|
+
extension_data=nested_data.get('extension_data'),
|
|
67
|
+
pay_elements_data=nested_data.get('salary_data'),
|
|
68
|
+
logical_id=logical_id,
|
|
69
|
+
pretty_print=pretty_print
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _flat_to_nested(self, flat_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Convert flat data to nested using PURE XSD schema inspection.
|
|
75
|
+
No hardcoded field mappings - everything is generic and schema-driven.
|
|
76
|
+
"""
|
|
77
|
+
result = {}
|
|
78
|
+
|
|
79
|
+
# Use the generic XSD-aware conversion directly on the main schema
|
|
80
|
+
nested_data = self._xsd_flat_to_nested(flat_data, IndicativeDataType)
|
|
81
|
+
|
|
82
|
+
# The result should have the indicative_person_dossier structure
|
|
83
|
+
if 'indicative_person_dossier' in nested_data:
|
|
84
|
+
# Extract the person dossier data
|
|
85
|
+
person_dossier = nested_data['indicative_person_dossier']
|
|
86
|
+
|
|
87
|
+
# Map the XSD structure to our expected output format
|
|
88
|
+
if person_dossier:
|
|
89
|
+
result['person_data'] = person_dossier
|
|
90
|
+
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def _xsd_flat_to_nested(self, flat_dict: Dict[str, Any], model: BaseModel) -> Dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Generic XSD-aware version of flat_dict_to_nested_dict.
|
|
96
|
+
Based on your Functions.flat_dict_to_nested_dict but handles XSD {"value": ...} format.
|
|
97
|
+
"""
|
|
98
|
+
nested = {}
|
|
99
|
+
|
|
100
|
+
for name, field in model.model_fields.items():
|
|
101
|
+
key_in_input = name # Original model field name as key in flat_dict
|
|
102
|
+
alias = field.alias or name
|
|
103
|
+
|
|
104
|
+
# Handle nested BaseModel fields (recursively)
|
|
105
|
+
if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
|
|
106
|
+
nested[alias] = self._xsd_flat_to_nested(flat_dict, field.annotation)
|
|
107
|
+
|
|
108
|
+
# Handle Union types with BaseModel (like Optional[SomeModel])
|
|
109
|
+
elif any(isinstance(item, type) and issubclass(item, BaseModel) for item in get_args(field.annotation)):
|
|
110
|
+
# Get the BaseModel class from the Union
|
|
111
|
+
nested_model = next(item for item in get_args(field.annotation)
|
|
112
|
+
if isinstance(item, type) and issubclass(item, BaseModel))
|
|
113
|
+
nested[alias] = self._xsd_flat_to_nested(flat_dict, nested_model)
|
|
114
|
+
|
|
115
|
+
# Handle List types with BaseModel elements
|
|
116
|
+
elif get_origin(field.annotation) is list:
|
|
117
|
+
args = get_args(field.annotation)
|
|
118
|
+
if args and isinstance(args[0], type) and issubclass(args[0], BaseModel):
|
|
119
|
+
# List of BaseModel - create single item with nested structure
|
|
120
|
+
nested_item = self._xsd_flat_to_nested(flat_dict, args[0])
|
|
121
|
+
if nested_item: # Only add if there's actual data
|
|
122
|
+
nested[alias] = [nested_item]
|
|
123
|
+
else:
|
|
124
|
+
# List of primitives - just wrap the value if present
|
|
125
|
+
if key_in_input in flat_dict:
|
|
126
|
+
nested[alias] = [flat_dict[key_in_input]]
|
|
127
|
+
|
|
128
|
+
# Handle primitive fields (including XSD value wrappers)
|
|
129
|
+
else:
|
|
130
|
+
if key_in_input in flat_dict:
|
|
131
|
+
value = flat_dict[key_in_input]
|
|
132
|
+
|
|
133
|
+
# Check if this is an XSD type that needs {"value": ...} wrapping
|
|
134
|
+
field_type = self._unwrap_optional_and_list(field.annotation)
|
|
135
|
+
if (hasattr(field_type, 'model_fields') and
|
|
136
|
+
isinstance(field_type.model_fields.get('value'), type)):
|
|
137
|
+
nested[alias] = {"value": value}
|
|
138
|
+
else:
|
|
139
|
+
nested[alias] = value
|
|
140
|
+
|
|
141
|
+
return nested
|
|
142
|
+
|
|
143
|
+
def _unwrap_optional_and_list(self, field_type):
|
|
144
|
+
"""Helper to unwrap Optional and List types to get the core type."""
|
|
145
|
+
# Handle Optional[T] (Union[T, None])
|
|
146
|
+
origin = get_origin(field_type)
|
|
147
|
+
if origin is type(Optional[str]) or str(origin) == 'typing.Union':
|
|
148
|
+
args = get_args(field_type)
|
|
149
|
+
if args:
|
|
150
|
+
for arg in args:
|
|
151
|
+
if arg is not type(None):
|
|
152
|
+
field_type = arg
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
# Handle List[T]
|
|
156
|
+
if get_origin(field_type) is list:
|
|
157
|
+
args = get_args(field_type)
|
|
158
|
+
if args:
|
|
159
|
+
field_type = args[0]
|
|
160
|
+
|
|
161
|
+
return field_type
|
|
162
|
+
|
|
163
|
+
def _build_person_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
|
|
164
|
+
"""Build person data structure using XSD-based automatic wrapping."""
|
|
165
|
+
person_data = {}
|
|
166
|
+
person_section = {}
|
|
167
|
+
employment_section = {}
|
|
168
|
+
|
|
169
|
+
# XSD-based automatic field processing
|
|
170
|
+
# These fields map directly to IndicativeDataType schema fields
|
|
171
|
+
direct_person_fields = ['person_id', 'birth_date']
|
|
172
|
+
direct_employment_fields = ['employee_id']
|
|
173
|
+
|
|
174
|
+
# Process direct person fields using XSD schema inspection
|
|
175
|
+
for field_name in direct_person_fields:
|
|
176
|
+
if fields.get(field_name):
|
|
177
|
+
wrapped_value = self._auto_wrap_field(field_name, fields[field_name], IndicativePerson)
|
|
178
|
+
person_section[field_name] = wrapped_value
|
|
179
|
+
|
|
180
|
+
# Process direct employment fields
|
|
181
|
+
for field_name in direct_employment_fields:
|
|
182
|
+
if fields.get(field_name):
|
|
183
|
+
wrapped_value = self._auto_wrap_field(field_name, fields[field_name], IndicativeDataType)
|
|
184
|
+
employment_section[field_name] = wrapped_value
|
|
185
|
+
|
|
186
|
+
# Handle structured fields that don't map directly to schema
|
|
187
|
+
# Names (complex nested structure)
|
|
188
|
+
if fields.get('first_name') or fields.get('last_name'):
|
|
189
|
+
person_section['person_name'] = [{}]
|
|
190
|
+
if fields.get('first_name'):
|
|
191
|
+
# Use XSD inspection for given_name wrapping
|
|
192
|
+
person_section['person_name'][0]['given_name'] = self._auto_wrap_field('given_name', fields['first_name'], IndicativeDataType)
|
|
193
|
+
if fields.get('last_name'):
|
|
194
|
+
# Use XSD inspection for family_name wrapping
|
|
195
|
+
person_section['person_name'][0]['family_name'] = self._auto_wrap_field('family_name', fields['last_name'], IndicativeDataType)
|
|
196
|
+
|
|
197
|
+
# Communication (structured)
|
|
198
|
+
communication = []
|
|
199
|
+
if fields.get('email'):
|
|
200
|
+
communication.append({
|
|
201
|
+
"type": "Email",
|
|
202
|
+
"uri": self._auto_wrap_field('uri', fields['email'], IndicativeDataType)
|
|
203
|
+
})
|
|
204
|
+
if fields.get('phone'):
|
|
205
|
+
communication.append({
|
|
206
|
+
"type": "Phone",
|
|
207
|
+
"dial_number": self._auto_wrap_field('dial_number', fields['phone'], IndicativeDataType)
|
|
208
|
+
})
|
|
209
|
+
if communication:
|
|
210
|
+
person_section['communication'] = communication
|
|
211
|
+
|
|
212
|
+
# Employment dates
|
|
213
|
+
hire_date = fields.get('hire_date')
|
|
214
|
+
effective_date = fields.get('effective_date')
|
|
215
|
+
actual_date = hire_date or effective_date
|
|
216
|
+
|
|
217
|
+
if actual_date:
|
|
218
|
+
employment_section['employment_lifecycle'] = [{
|
|
219
|
+
"valid_from": actual_date,
|
|
220
|
+
"hire": {
|
|
221
|
+
"hire_date": self._auto_wrap_field('hire_date', actual_date, IndicativeDataType),
|
|
222
|
+
"original_hire_date": self._auto_wrap_field('original_hire_date', actual_date, IndicativeDataType)
|
|
223
|
+
}
|
|
224
|
+
}]
|
|
225
|
+
|
|
226
|
+
# Assemble
|
|
227
|
+
if person_section:
|
|
228
|
+
person_data['person'] = person_section
|
|
229
|
+
if employment_section:
|
|
230
|
+
person_data['employment'] = employment_section
|
|
231
|
+
|
|
232
|
+
return person_data
|
|
233
|
+
|
|
234
|
+
def _build_extension_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
|
|
235
|
+
"""Build extension data (payment info, etc.)."""
|
|
236
|
+
extension_data = {}
|
|
237
|
+
|
|
238
|
+
# Payment instruction
|
|
239
|
+
if any(fields.get(k) for k in ['iban', 'bic', 'account_holder']):
|
|
240
|
+
extension_data['payment_instruction'] = {}
|
|
241
|
+
if fields.get('iban'):
|
|
242
|
+
extension_data['payment_instruction']['iban'] = fields['iban']
|
|
243
|
+
if fields.get('bic'):
|
|
244
|
+
extension_data['payment_instruction']['bic'] = fields['bic']
|
|
245
|
+
if fields.get('account_holder'):
|
|
246
|
+
extension_data['payment_instruction']['holder_name'] = fields['account_holder']
|
|
247
|
+
|
|
248
|
+
if fields.get('notes'):
|
|
249
|
+
extension_data['note'] = fields['notes']
|
|
250
|
+
|
|
251
|
+
return extension_data
|
|
252
|
+
|
|
253
|
+
def _build_salary_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
|
|
254
|
+
"""Build salary data."""
|
|
255
|
+
salary_data = {}
|
|
256
|
+
pay_elements = []
|
|
257
|
+
|
|
258
|
+
if fields.get('base_salary'):
|
|
259
|
+
pay_elements.append({
|
|
260
|
+
"code": "BASE",
|
|
261
|
+
"amount": fields['base_salary'],
|
|
262
|
+
"valid_from": fields.get('effective_date') or fields.get('hire_date')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
if fields.get('bonus_amount'):
|
|
266
|
+
pay_elements.append({
|
|
267
|
+
"code": "BONUS",
|
|
268
|
+
"amount": fields['bonus_amount'],
|
|
269
|
+
"valid_from": fields.get('effective_date') or fields.get('hire_date')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
if pay_elements:
|
|
273
|
+
salary_data['pay_element'] = pay_elements
|
|
274
|
+
|
|
275
|
+
return salary_data
|
|
276
|
+
|
|
277
|
+
def _build_job_data(self, job_fields: Dict[str, Any], all_fields: Dict[str, Any]) -> Dict[str, Any]:
|
|
278
|
+
"""Build job/deployment data."""
|
|
279
|
+
job_data = {}
|
|
280
|
+
|
|
281
|
+
actual_date = all_fields.get('hire_date') or all_fields.get('effective_date')
|
|
282
|
+
if actual_date:
|
|
283
|
+
job_data['valid_from'] = actual_date
|
|
284
|
+
|
|
285
|
+
if job_fields.get('job_title'):
|
|
286
|
+
job_data['job'] = {"title": job_fields['job_title']}
|
|
287
|
+
|
|
288
|
+
if job_fields.get('weekly_hours'):
|
|
289
|
+
job_data['schedule'] = {"weekly_hours": job_fields['weekly_hours']}
|
|
290
|
+
|
|
291
|
+
if job_fields.get('country'):
|
|
292
|
+
job_data['work_location'] = {"country": job_fields['country']}
|
|
293
|
+
|
|
294
|
+
return job_data
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simplified manager for the Alight SDK.
|
|
3
|
+
Pure dictionary transformations - no unnecessary classes or complexity.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Optional, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from . import Alight
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EmployeeManager:
|
|
13
|
+
"""Simple employee manager - just flat-to-nested dictionary conversion."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, alight_sdk: 'Alight'):
|
|
16
|
+
self.alight_sdk = alight_sdk
|
|
17
|
+
|
|
18
|
+
# Simple field categorization
|
|
19
|
+
self.field_categories = {
|
|
20
|
+
'extension': ['iban', 'bic', 'account_holder', 'notes'],
|
|
21
|
+
'salary': ['base_salary', 'bonus_amount', 'salary_currency', 'pay_element_code'],
|
|
22
|
+
'job': ['job_title', 'weekly_hours', 'country'],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def create_newhire_simple(self, flat_data: Dict[str, Any],
|
|
26
|
+
logical_id: Optional[str] = None,
|
|
27
|
+
pretty_print: bool = True,
|
|
28
|
+
save_to_file: bool = False,
|
|
29
|
+
filename: Optional[str] = None) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Create NewHire XML from flat dictionary - that's it!
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
flat_data: Simple flat dictionary with snake_case keys
|
|
35
|
+
logical_id: Optional logical ID
|
|
36
|
+
pretty_print: Format XML nicely
|
|
37
|
+
save_to_file: Save to file
|
|
38
|
+
filename: Optional filename
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str: HR-XML ready for Alight
|
|
42
|
+
"""
|
|
43
|
+
nested_data = self._flat_to_nested(flat_data)
|
|
44
|
+
return self.alight_sdk.generate_newhire_xml(
|
|
45
|
+
person_data=nested_data.get('person_data'),
|
|
46
|
+
extension_data=nested_data.get('extension_data'),
|
|
47
|
+
pay_elements_data=nested_data.get('salary_data'),
|
|
48
|
+
logical_id=logical_id,
|
|
49
|
+
pretty_print=pretty_print
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def create_employee_change_simple(self, flat_data: Dict[str, Any],
|
|
53
|
+
logical_id: Optional[str] = None,
|
|
54
|
+
pretty_print: bool = True,
|
|
55
|
+
save_to_file: bool = False,
|
|
56
|
+
filename: Optional[str] = None) -> str:
|
|
57
|
+
"""
|
|
58
|
+
Create Employee Change XML from flat dictionary - that's it!
|
|
59
|
+
"""
|
|
60
|
+
nested_data = self._flat_to_nested(flat_data)
|
|
61
|
+
return self.alight_sdk.generate_employee_change_xml(
|
|
62
|
+
person_data=nested_data.get('person_data'),
|
|
63
|
+
extension_data=nested_data.get('extension_data'),
|
|
64
|
+
pay_elements_data=nested_data.get('salary_data'),
|
|
65
|
+
logical_id=logical_id,
|
|
66
|
+
pretty_print=pretty_print
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _flat_to_nested(self, flat_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
70
|
+
"""Convert flat data to nested - the ONLY transformation we need."""
|
|
71
|
+
result = {}
|
|
72
|
+
|
|
73
|
+
# Categorize fields
|
|
74
|
+
person_fields = {}
|
|
75
|
+
extension_fields = {}
|
|
76
|
+
salary_fields = {}
|
|
77
|
+
job_fields = {}
|
|
78
|
+
|
|
79
|
+
for key, value in flat_data.items():
|
|
80
|
+
if key in self.field_categories['extension']:
|
|
81
|
+
extension_fields[key] = value
|
|
82
|
+
elif key in self.field_categories['salary']:
|
|
83
|
+
salary_fields[key] = value
|
|
84
|
+
elif key in self.field_categories['job']:
|
|
85
|
+
job_fields[key] = value
|
|
86
|
+
else:
|
|
87
|
+
person_fields[key] = value
|
|
88
|
+
|
|
89
|
+
# Build structures (simple dictionary operations)
|
|
90
|
+
if person_fields:
|
|
91
|
+
result['person_data'] = self._build_person_data(person_fields)
|
|
92
|
+
|
|
93
|
+
if extension_fields:
|
|
94
|
+
result['extension_data'] = self._build_extension_data(extension_fields)
|
|
95
|
+
|
|
96
|
+
if salary_fields:
|
|
97
|
+
result['salary_data'] = self._build_salary_data(salary_fields)
|
|
98
|
+
|
|
99
|
+
if job_fields:
|
|
100
|
+
# Add job data to person_data (deployment section)
|
|
101
|
+
if 'person_data' not in result:
|
|
102
|
+
result['person_data'] = {}
|
|
103
|
+
result['person_data']['deployment'] = self._build_job_data(job_fields, flat_data)
|
|
104
|
+
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
def _build_person_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
|
|
108
|
+
"""Build person data structure with automatic {"value": ...} wrapping."""
|
|
109
|
+
person_data = {}
|
|
110
|
+
person_section = {}
|
|
111
|
+
employment_section = {}
|
|
112
|
+
|
|
113
|
+
# Person identification
|
|
114
|
+
if fields.get('person_id'):
|
|
115
|
+
person_section['person_id'] = [{"value": fields['person_id']}]
|
|
116
|
+
|
|
117
|
+
# Names
|
|
118
|
+
if fields.get('first_name') or fields.get('last_name'):
|
|
119
|
+
person_section['person_name'] = [{}]
|
|
120
|
+
if fields.get('first_name'):
|
|
121
|
+
person_section['person_name'][0]['given_name'] = [{"value": fields['first_name']}]
|
|
122
|
+
if fields.get('last_name'):
|
|
123
|
+
person_section['person_name'][0]['family_name'] = [{"value": fields['last_name']}]
|
|
124
|
+
|
|
125
|
+
# Birth date
|
|
126
|
+
if fields.get('birth_date'):
|
|
127
|
+
person_section['birth_date'] = {"value": fields['birth_date']}
|
|
128
|
+
|
|
129
|
+
# Contact info
|
|
130
|
+
if fields.get('email'):
|
|
131
|
+
person_section['communication'] = person_section.get('communication', [])
|
|
132
|
+
person_section['communication'].append({
|
|
133
|
+
"type": "Email",
|
|
134
|
+
"uri": {"value": fields['email']}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
if fields.get('phone'):
|
|
138
|
+
person_section['communication'] = person_section.get('communication', [])
|
|
139
|
+
person_section['communication'].append({
|
|
140
|
+
"type": "Phone",
|
|
141
|
+
"dial_number": {"value": fields['phone']}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
# Employment dates
|
|
145
|
+
hire_date = fields.get('hire_date')
|
|
146
|
+
effective_date = fields.get('effective_date')
|
|
147
|
+
actual_date = hire_date or effective_date
|
|
148
|
+
|
|
149
|
+
if actual_date:
|
|
150
|
+
employment_section['employment_lifecycle'] = [{
|
|
151
|
+
"valid_from": actual_date,
|
|
152
|
+
"hire": {
|
|
153
|
+
"hire_date": {"value": actual_date},
|
|
154
|
+
"original_hire_date": {"value": actual_date}
|
|
155
|
+
}
|
|
156
|
+
}]
|
|
157
|
+
|
|
158
|
+
if fields.get('employee_id'):
|
|
159
|
+
employment_section['employee_id'] = [{"value": fields['employee_id']}]
|
|
160
|
+
|
|
161
|
+
# Assemble
|
|
162
|
+
if person_section:
|
|
163
|
+
person_data['person'] = person_section
|
|
164
|
+
if employment_section:
|
|
165
|
+
person_data['employment'] = employment_section
|
|
166
|
+
|
|
167
|
+
return person_data
|
|
168
|
+
|
|
169
|
+
def _build_extension_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
|
|
170
|
+
"""Build extension data (payment info, etc.)."""
|
|
171
|
+
extension_data = {}
|
|
172
|
+
|
|
173
|
+
# Payment instruction
|
|
174
|
+
if any(fields.get(k) for k in ['iban', 'bic', 'account_holder']):
|
|
175
|
+
extension_data['payment_instruction'] = {}
|
|
176
|
+
if fields.get('iban'):
|
|
177
|
+
extension_data['payment_instruction']['iban'] = fields['iban']
|
|
178
|
+
if fields.get('bic'):
|
|
179
|
+
extension_data['payment_instruction']['bic'] = fields['bic']
|
|
180
|
+
if fields.get('account_holder'):
|
|
181
|
+
extension_data['payment_instruction']['holder_name'] = fields['account_holder']
|
|
182
|
+
|
|
183
|
+
if fields.get('notes'):
|
|
184
|
+
extension_data['note'] = fields['notes']
|
|
185
|
+
|
|
186
|
+
return extension_data
|
|
187
|
+
|
|
188
|
+
def _build_salary_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
|
|
189
|
+
"""Build salary data."""
|
|
190
|
+
salary_data = {}
|
|
191
|
+
pay_elements = []
|
|
192
|
+
|
|
193
|
+
if fields.get('base_salary'):
|
|
194
|
+
pay_elements.append({
|
|
195
|
+
"code": "BASE",
|
|
196
|
+
"amount": fields['base_salary'],
|
|
197
|
+
"valid_from": fields.get('effective_date') or fields.get('hire_date')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
if fields.get('bonus_amount'):
|
|
201
|
+
pay_elements.append({
|
|
202
|
+
"code": "BONUS",
|
|
203
|
+
"amount": fields['bonus_amount'],
|
|
204
|
+
"valid_from": fields.get('effective_date') or fields.get('hire_date')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
if pay_elements:
|
|
208
|
+
salary_data['pay_element'] = pay_elements
|
|
209
|
+
|
|
210
|
+
return salary_data
|
|
211
|
+
|
|
212
|
+
def _build_job_data(self, job_fields: Dict[str, Any], all_fields: Dict[str, Any]) -> Dict[str, Any]:
|
|
213
|
+
"""Build job/deployment data."""
|
|
214
|
+
job_data = {}
|
|
215
|
+
|
|
216
|
+
actual_date = all_fields.get('hire_date') or all_fields.get('effective_date')
|
|
217
|
+
if actual_date:
|
|
218
|
+
job_data['valid_from'] = actual_date
|
|
219
|
+
|
|
220
|
+
if job_fields.get('job_title'):
|
|
221
|
+
job_data['job'] = {"title": job_fields['job_title']}
|
|
222
|
+
|
|
223
|
+
if job_fields.get('weekly_hours'):
|
|
224
|
+
job_data['schedule'] = {"weekly_hours": job_fields['weekly_hours']}
|
|
225
|
+
|
|
226
|
+
if job_fields.get('country'):
|
|
227
|
+
job_data['work_location'] = {"country": job_fields['country']}
|
|
228
|
+
|
|
229
|
+
return job_data
|