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,72 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .schemas import Address as AddressModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Address:
|
|
7
|
+
"""
|
|
8
|
+
High-level Address API. Address changes are updates to IndicativeData.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
alight = Alight()
|
|
12
|
+
xml = alight.address.update(
|
|
13
|
+
data={...},
|
|
14
|
+
person_id="35561",
|
|
15
|
+
employee_id="35561ZZGB",
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, client: Any):
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
def update(
|
|
23
|
+
self,
|
|
24
|
+
data: Dict[str, Any],
|
|
25
|
+
*,
|
|
26
|
+
person_id: str,
|
|
27
|
+
employee_id: str,
|
|
28
|
+
logical_id: Optional[str] = None,
|
|
29
|
+
envelope_options: Optional[Dict[str, Any]] = None,
|
|
30
|
+
employee_data: Optional[Dict[str, Any]] = None,
|
|
31
|
+
pretty_print: bool = True,
|
|
32
|
+
) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Update address (actionCode=CHANGE). Returns XML string.
|
|
35
|
+
Requires both person_id and employee_id per integration constraints.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> alight.address.update(
|
|
39
|
+
... data={
|
|
40
|
+
... "address_line_1": "10 Downing Street",
|
|
41
|
+
... "town": "London",
|
|
42
|
+
... "postal_code": "SW1A 2AA",
|
|
43
|
+
... "address_valid_from": "2024-01-01",
|
|
44
|
+
... },
|
|
45
|
+
... person_id="35561",
|
|
46
|
+
... employee_id="35561ZZGB",
|
|
47
|
+
... )
|
|
48
|
+
"""
|
|
49
|
+
payload: dict[str, Any] = {
|
|
50
|
+
"person_id": person_id,
|
|
51
|
+
"employee_id": employee_id,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# build from flat fields
|
|
55
|
+
address_model = AddressModel(**data)
|
|
56
|
+
payload["address"] = address_model.model_dump(exclude_none=True, by_alias=True)
|
|
57
|
+
|
|
58
|
+
if "address_valid_from" in data:
|
|
59
|
+
payload["address_valid_from"] = data["address_valid_from"]
|
|
60
|
+
if "address_valid_to" in data:
|
|
61
|
+
payload["address_valid_to"] = data["address_valid_to"]
|
|
62
|
+
|
|
63
|
+
if employee_data:
|
|
64
|
+
payload.update(employee_data)
|
|
65
|
+
|
|
66
|
+
return self._client.generate_employee_xml(
|
|
67
|
+
employee=payload,
|
|
68
|
+
action_code="CHANGE",
|
|
69
|
+
logical_id=logical_id,
|
|
70
|
+
envelope_options=envelope_options,
|
|
71
|
+
pretty_print=pretty_print,
|
|
72
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flat wrapper for XSD schemas.
|
|
3
|
+
Provides a simple interface that automatically maps flat dictionaries to complex XSD schemas.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional, Type, get_origin, get_args
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from xsdata.models.datatype import XmlDate
|
|
9
|
+
from xsdata_pydantic.bindings import XmlSerializer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SchemaWrapper:
|
|
13
|
+
"""
|
|
14
|
+
Generic wrapper for any XSD schema.
|
|
15
|
+
Converts flat dictionaries to proper XSD format without field duplication.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
employee_data = {
|
|
19
|
+
"person_id": "12345",
|
|
20
|
+
"given_name": "John",
|
|
21
|
+
"family_name": "Smith",
|
|
22
|
+
"birth_date": "1990-01-01",
|
|
23
|
+
# ... any other fields matching the schema
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Create wrapper with the flat data and schema class
|
|
27
|
+
wrapper = SchemaWrapper(employee_data, IndicativeDataType)
|
|
28
|
+
|
|
29
|
+
# Get XML directly
|
|
30
|
+
xml = wrapper.to_xml()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, data: Dict[str, Any], schema_class: Type[BaseModel]):
|
|
34
|
+
"""
|
|
35
|
+
Initialize with flat data and target schema class.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
data: Flat dictionary with field names matching schema fields
|
|
39
|
+
schema_class: XSD-generated schema class to use (e.g. IndicativeDataType)
|
|
40
|
+
"""
|
|
41
|
+
self.data = data
|
|
42
|
+
self.schema_class = schema_class
|
|
43
|
+
|
|
44
|
+
def _xsd_flat_to_nested(self, flat_dict: Dict[str, Any], model: Type[BaseModel]) -> Dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Convert flat dictionary to nested structure using schema introspection.
|
|
47
|
+
Uses the model's field types to determine how to structure the data.
|
|
48
|
+
"""
|
|
49
|
+
nested = {}
|
|
50
|
+
|
|
51
|
+
for name, field in model.model_fields.items():
|
|
52
|
+
key_in_input = name # Original model field name as key in flat_dict
|
|
53
|
+
alias = field.alias or name
|
|
54
|
+
|
|
55
|
+
# Handle nested BaseModel fields (recursively)
|
|
56
|
+
if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
|
|
57
|
+
nested[alias] = self._xsd_flat_to_nested(flat_dict, field.annotation)
|
|
58
|
+
|
|
59
|
+
# Handle Union types with BaseModel (like Optional[SomeModel])
|
|
60
|
+
elif any(isinstance(item, type) and issubclass(item, BaseModel)
|
|
61
|
+
for item in get_args(field.annotation) if isinstance(item, type)):
|
|
62
|
+
# Get the BaseModel class from the Union
|
|
63
|
+
nested_model = next((item for item in get_args(field.annotation)
|
|
64
|
+
if isinstance(item, type) and issubclass(item, BaseModel)), None)
|
|
65
|
+
if nested_model:
|
|
66
|
+
nested[alias] = self._xsd_flat_to_nested(flat_dict, nested_model)
|
|
67
|
+
|
|
68
|
+
# Handle primitive fields - PURE schema introspection, NO hardcoding!
|
|
69
|
+
else:
|
|
70
|
+
if key_in_input in flat_dict:
|
|
71
|
+
value = flat_dict[key_in_input]
|
|
72
|
+
|
|
73
|
+
# XSD enhancement: Check if this field type needs {"value": ...} wrapping
|
|
74
|
+
field_type = self._unwrap_optional_and_list(field.annotation)
|
|
75
|
+
if (hasattr(field_type, 'model_fields') and
|
|
76
|
+
'value' in getattr(field_type, 'model_fields', {})):
|
|
77
|
+
# Automatic XmlDate conversion for date fields (minimal logic)
|
|
78
|
+
if 'date' in key_in_input.lower() and isinstance(value, str):
|
|
79
|
+
nested[alias] = {"value": XmlDate.from_string(value)}
|
|
80
|
+
else:
|
|
81
|
+
nested[alias] = {"value": value}
|
|
82
|
+
else:
|
|
83
|
+
nested[alias] = value
|
|
84
|
+
|
|
85
|
+
return nested
|
|
86
|
+
|
|
87
|
+
def _unwrap_optional_and_list(self, field_type):
|
|
88
|
+
"""Helper to unwrap Optional and List types to get the core type."""
|
|
89
|
+
# Handle Optional[T] (Union[T, None])
|
|
90
|
+
origin = get_origin(field_type)
|
|
91
|
+
if origin is type(None) or str(origin) == 'typing.Union':
|
|
92
|
+
args = get_args(field_type)
|
|
93
|
+
if args:
|
|
94
|
+
for arg in args:
|
|
95
|
+
if arg is not type(None):
|
|
96
|
+
field_type = arg
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
# Handle List[T]
|
|
100
|
+
if get_origin(field_type) is list:
|
|
101
|
+
args = get_args(field_type)
|
|
102
|
+
if args:
|
|
103
|
+
field_type = args[0]
|
|
104
|
+
|
|
105
|
+
return field_type
|
|
106
|
+
|
|
107
|
+
def to_nested_dict(self) -> Dict[str, Any]:
|
|
108
|
+
"""Convert flat data to nested dictionary structure using schema introspection."""
|
|
109
|
+
return self._xsd_flat_to_nested(self.data, self.schema_class)
|
|
110
|
+
|
|
111
|
+
def to_model(self) -> BaseModel:
|
|
112
|
+
"""Convert flat data to proper schema model instance using introspection."""
|
|
113
|
+
nested_data = self._xsd_flat_to_nested(self.data, self.schema_class)
|
|
114
|
+
return self.schema_class.model_validate(nested_data)
|
|
115
|
+
|
|
116
|
+
def to_xml(self, pretty_print: bool = True) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Generate XML output directly from flat data.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
pretty_print: Whether to format the XML with proper indentation
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
XML string representation of the data
|
|
125
|
+
"""
|
|
126
|
+
from xml.dom import minidom
|
|
127
|
+
|
|
128
|
+
model = self.to_model()
|
|
129
|
+
serializer = XmlSerializer()
|
|
130
|
+
xml_output = serializer.render(model)
|
|
131
|
+
|
|
132
|
+
# Optional pretty printing
|
|
133
|
+
if pretty_print:
|
|
134
|
+
dom = minidom.parseString(xml_output)
|
|
135
|
+
xml_output = dom.toprettyxml(indent=" ")
|
|
136
|
+
xml_output = '\n'.join([line for line in xml_output.split('\n') if line.strip()])
|
|
137
|
+
|
|
138
|
+
return xml_output
|
|
139
|
+
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HR-XML Generator for Alight Integration
|
|
3
|
+
|
|
4
|
+
This module provides functions to generate HR-XML documents using XSData models
|
|
5
|
+
with proper Alight envelope structure and namespace handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
from xml.dom import minidom
|
|
12
|
+
|
|
13
|
+
from xsdata.models.datatype import XmlDate, XmlDateTime
|
|
14
|
+
from xsdata_pydantic.bindings import XmlSerializer as PydanticXmlSerializer
|
|
15
|
+
|
|
16
|
+
from .schemas.hrxml_indicative_data import IndicativeDataType, IndicativeData
|
|
17
|
+
from .schemas.flattened_main_schemas.process_pay_serv_emp import (
|
|
18
|
+
ProcessPayServEmp, DataArea, PayServEmp, PayServEmpExtension, PayServEmpPayElements
|
|
19
|
+
)
|
|
20
|
+
from .schemas.openapplications_bod import ApplicationArea, Sender
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_hrxml_from_data(person_data: Dict[str, Any]) -> IndicativeDataType:
|
|
24
|
+
"""
|
|
25
|
+
Create HR-XML IndicativeDataType from person data dictionary.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
person_data: Dictionary containing person, employee, employment, deployment, and remuneration data
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
IndicativeDataType: Validated XSData model instance
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
indicative_data = IndicativeDataType.model_validate(person_data)
|
|
35
|
+
return indicative_data
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise ValueError(f"Failed to create HR-XML from data: {e}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_extension_from_data(extension_data: Dict[str, Any]) -> PayServEmpExtension:
|
|
41
|
+
"""
|
|
42
|
+
Create PayServEmpExtension from extension data dictionary.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
extension_data: Dictionary containing payment instructions, cost assignments, etc.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
PayServEmpExtension: Validated XSData model instance
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
extension = PayServEmpExtension.model_validate(extension_data)
|
|
52
|
+
return extension
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise ValueError(f"Failed to create extension from data: {e}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_pay_elements_from_data(pay_elements_data: Dict[str, Any]) -> PayServEmpPayElements:
|
|
58
|
+
"""
|
|
59
|
+
Create PayServEmpPayElements from pay elements data dictionary.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
pay_elements_data: Dictionary containing pay elements
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
PayServEmpPayElements: Validated XSData model instance
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
pay_elements = PayServEmpPayElements.model_validate(pay_elements_data)
|
|
69
|
+
return pay_elements
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise ValueError(f"Failed to create pay elements from data: {e}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_complete_alight_envelope(
|
|
75
|
+
indicative_data: IndicativeDataType,
|
|
76
|
+
extension: Optional[PayServEmpExtension] = None,
|
|
77
|
+
pay_elements: Optional[PayServEmpPayElements] = None,
|
|
78
|
+
action_code: str = "ADD",
|
|
79
|
+
logical_id: str = "TST-GB003-1001",
|
|
80
|
+
language_code: str = "en-US",
|
|
81
|
+
system_environment_code: str = "TST NF",
|
|
82
|
+
release_id: str = "DEFAULT"
|
|
83
|
+
) -> ProcessPayServEmp:
|
|
84
|
+
"""
|
|
85
|
+
Create the complete Alight XML envelope using XSData models.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
indicative_data: The HR-XML indicative data
|
|
89
|
+
extension: Optional Alight-specific extensions
|
|
90
|
+
pay_elements: Optional pay elements
|
|
91
|
+
action_code: Action code for the process (default: "ADD")
|
|
92
|
+
logical_id: Logical ID for the sender (default: "TST-GB003-1001")
|
|
93
|
+
language_code: Language code (default: "en-US")
|
|
94
|
+
system_environment_code: System environment (default: "TST NF")
|
|
95
|
+
release_id: Release ID (default: "DEFAULT")
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
ProcessPayServEmp: Complete envelope ready for serialization
|
|
99
|
+
"""
|
|
100
|
+
# Create timestamp and BODID
|
|
101
|
+
timestamp = datetime.now()
|
|
102
|
+
bod_id = str(uuid.uuid4()).upper()
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Create Sender
|
|
106
|
+
sender_data = {
|
|
107
|
+
"logical_id": {"value": logical_id},
|
|
108
|
+
"component_id": {"value": "PAYROLL"},
|
|
109
|
+
"reference_id": {"value": "hrisxml"},
|
|
110
|
+
"confirmation_code": {"value": "Always"}
|
|
111
|
+
}
|
|
112
|
+
sender = Sender.model_validate(sender_data)
|
|
113
|
+
|
|
114
|
+
# Create ApplicationArea
|
|
115
|
+
app_area_data = {
|
|
116
|
+
"sender": sender,
|
|
117
|
+
"creation_date_time": {"value": XmlDateTime.from_datetime(timestamp)},
|
|
118
|
+
"bodid": {"value": bod_id}
|
|
119
|
+
}
|
|
120
|
+
application_area = ApplicationArea.model_validate(app_area_data)
|
|
121
|
+
|
|
122
|
+
# Create PayServEmp (contains our IndicativeData + Extensions)
|
|
123
|
+
# Need to create IndicativeData wrapper (not IndicativeDataType)
|
|
124
|
+
indicative_data_wrapper = IndicativeData(
|
|
125
|
+
indicative_person_dossier=indicative_data.indicative_person_dossier
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
pay_serv_emp_data = {
|
|
129
|
+
"indicative_data": indicative_data_wrapper
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Add optional components if provided
|
|
133
|
+
if extension:
|
|
134
|
+
pay_serv_emp_data["pay_serv_emp_extension"] = extension
|
|
135
|
+
if pay_elements:
|
|
136
|
+
pay_serv_emp_data["pay_serv_emp_pay_elements"] = pay_elements
|
|
137
|
+
|
|
138
|
+
pay_serv_emp = PayServEmp.model_validate(pay_serv_emp_data)
|
|
139
|
+
|
|
140
|
+
# Create Process with ActionExpression
|
|
141
|
+
process_data = {
|
|
142
|
+
"action_criteria": [{
|
|
143
|
+
"action_expression": [{
|
|
144
|
+
"action_code": action_code
|
|
145
|
+
}]
|
|
146
|
+
}]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Create DataArea
|
|
150
|
+
data_area_data = {
|
|
151
|
+
"process": process_data,
|
|
152
|
+
"pay_serv_emp": [pay_serv_emp]
|
|
153
|
+
}
|
|
154
|
+
data_area = DataArea.model_validate(data_area_data)
|
|
155
|
+
|
|
156
|
+
# Create the complete ProcessPayServEmp envelope
|
|
157
|
+
envelope_data = {
|
|
158
|
+
"application_area": application_area,
|
|
159
|
+
"data_area": data_area,
|
|
160
|
+
"language_code": language_code,
|
|
161
|
+
"system_environment_code": system_environment_code,
|
|
162
|
+
"release_id": release_id
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
complete_envelope = ProcessPayServEmp.model_validate(envelope_data)
|
|
166
|
+
return complete_envelope
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
raise ValueError(f"Failed to create complete Alight envelope: {e}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def serialize_with_namespaces(
|
|
173
|
+
envelope: ProcessPayServEmp,
|
|
174
|
+
pretty_print: bool = True,
|
|
175
|
+
custom_namespaces: Optional[Dict[str, str]] = None
|
|
176
|
+
) -> str:
|
|
177
|
+
"""
|
|
178
|
+
Serialize ProcessPayServEmp envelope to XML with proper namespace handling.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
envelope: The ProcessPayServEmp envelope to serialize
|
|
182
|
+
pretty_print: Whether to format the XML with indentation (default: True)
|
|
183
|
+
custom_namespaces: Custom namespace mappings (prefix -> namespace URI)
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
str: Serialized XML string
|
|
187
|
+
"""
|
|
188
|
+
# Default namespace mappings for Alight standard prefixes
|
|
189
|
+
namespace_map = custom_namespaces or {
|
|
190
|
+
"nga": "http://www.ngahr.com/ngapexxml/1",
|
|
191
|
+
"oa": "http://www.openapplications.org/oagis/9",
|
|
192
|
+
"hr": "http://www.hr-xml.org/3"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Use ns_map parameter for clean namespace mapping
|
|
197
|
+
pydantic_serializer = PydanticXmlSerializer()
|
|
198
|
+
raw_xml = pydantic_serializer.render(envelope, ns_map=namespace_map)
|
|
199
|
+
|
|
200
|
+
if pretty_print:
|
|
201
|
+
# Pretty print the XML
|
|
202
|
+
dom = minidom.parseString(raw_xml)
|
|
203
|
+
pretty_xml = dom.toprettyxml(indent=" ", encoding=None)
|
|
204
|
+
|
|
205
|
+
# Clean up extra blank lines
|
|
206
|
+
lines = [line for line in pretty_xml.split('\n') if line.strip()]
|
|
207
|
+
return '\n'.join(lines)
|
|
208
|
+
else:
|
|
209
|
+
return raw_xml
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
# Fallback without ns_map if it fails
|
|
213
|
+
try:
|
|
214
|
+
pydantic_serializer = PydanticXmlSerializer()
|
|
215
|
+
raw_xml = pydantic_serializer.render(envelope)
|
|
216
|
+
|
|
217
|
+
if pretty_print:
|
|
218
|
+
dom = minidom.parseString(raw_xml)
|
|
219
|
+
pretty_xml = dom.toprettyxml(indent=" ", encoding=None)
|
|
220
|
+
lines = [line for line in pretty_xml.split('\n') if line.strip()]
|
|
221
|
+
return '\n'.join(lines)
|
|
222
|
+
else:
|
|
223
|
+
return raw_xml
|
|
224
|
+
|
|
225
|
+
except Exception as e2:
|
|
226
|
+
raise ValueError(f"Failed to serialize envelope: {e2}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def generate_complete_hrxml(
|
|
230
|
+
person_data: Dict[str, Any],
|
|
231
|
+
extension_data: Optional[Dict[str, Any]] = None,
|
|
232
|
+
pay_elements_data: Optional[Dict[str, Any]] = None,
|
|
233
|
+
action_code: str = "ADD",
|
|
234
|
+
logical_id: str = "TST-GB003-1001",
|
|
235
|
+
pretty_print: bool = True
|
|
236
|
+
) -> str:
|
|
237
|
+
"""
|
|
238
|
+
High-level function to generate complete Alight HR-XML from data dictionaries.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
person_data: Dictionary containing person/employee/employment data
|
|
242
|
+
extension_data: Optional dictionary containing Alight extensions
|
|
243
|
+
pay_elements_data: Optional dictionary containing pay elements
|
|
244
|
+
action_code: Action code for the process (default: "ADD")
|
|
245
|
+
logical_id: Logical ID for the sender (default: "TST-GB003-1001")
|
|
246
|
+
pretty_print: Whether to format the XML with indentation (default: True)
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
str: Complete Alight HR-XML string
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
# Create HR-XML content
|
|
253
|
+
indicative_data = create_hrxml_from_data(person_data)
|
|
254
|
+
|
|
255
|
+
# Create optional components
|
|
256
|
+
extension = None
|
|
257
|
+
pay_elements = None
|
|
258
|
+
|
|
259
|
+
if extension_data:
|
|
260
|
+
extension = create_extension_from_data(extension_data)
|
|
261
|
+
|
|
262
|
+
if pay_elements_data:
|
|
263
|
+
pay_elements = create_pay_elements_from_data(pay_elements_data)
|
|
264
|
+
|
|
265
|
+
# Create complete envelope
|
|
266
|
+
envelope = create_complete_alight_envelope(
|
|
267
|
+
indicative_data=indicative_data,
|
|
268
|
+
extension=extension,
|
|
269
|
+
pay_elements=pay_elements,
|
|
270
|
+
action_code=action_code,
|
|
271
|
+
logical_id=logical_id
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Serialize to XML
|
|
275
|
+
xml_string = serialize_with_namespaces(envelope, pretty_print=pretty_print)
|
|
276
|
+
|
|
277
|
+
return xml_string
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
raise ValueError(f"Failed to generate complete HR-XML: {e}")
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
# Check if data is already in schema-compliant format (has proper nesting)
|
|
57
|
+
if self._is_already_schema_compliant(flat_data):
|
|
58
|
+
if self.alight_sdk.debug:
|
|
59
|
+
print("✅ Data is already schema-compliant, using directly")
|
|
60
|
+
return flat_data
|
|
61
|
+
|
|
62
|
+
# Use the generic XSD-aware conversion for truly flat data
|
|
63
|
+
return self._xsd_flat_to_nested(flat_data, IndicativeDataType)
|
|
64
|
+
|
|
65
|
+
def _is_already_schema_compliant(self, data: Dict[str, Any]) -> bool:
|
|
66
|
+
"""Check if data is already in schema-compliant format."""
|
|
67
|
+
# Look for key indicators that it's already structured correctly
|
|
68
|
+
if "indicative_person_dossier" in data:
|
|
69
|
+
person_dossier = data["indicative_person_dossier"]
|
|
70
|
+
# Check if main sections are lists (as required by schema)
|
|
71
|
+
if (isinstance(person_dossier.get("indicative_person"), list) and
|
|
72
|
+
isinstance(person_dossier.get("indicative_employee"), list) and
|
|
73
|
+
isinstance(person_dossier.get("indicative_employment"), list)):
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def _xsd_flat_to_nested(self, flat_dict: Dict[str, Any], model: BaseModel) -> Dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
Generic XSD-aware version exactly like your Functions.flat_dict_to_nested_dict.
|
|
80
|
+
Recursively processes the schema and handles XSD {"value": ...} format automatically.
|
|
81
|
+
"""
|
|
82
|
+
nested = {}
|
|
83
|
+
|
|
84
|
+
for name, field in model.model_fields.items():
|
|
85
|
+
key_in_input = name # Original model field name as key in flat_dict
|
|
86
|
+
alias = field.alias or name
|
|
87
|
+
|
|
88
|
+
# Handle nested BaseModel fields (recursively) - exact same logic as your function
|
|
89
|
+
if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
|
|
90
|
+
nested[alias] = self._xsd_flat_to_nested(flat_dict, field.annotation)
|
|
91
|
+
|
|
92
|
+
# Handle Union types with BaseModel (like Optional[SomeModel]) - exact same logic
|
|
93
|
+
elif any(isinstance(item, type) and issubclass(item, BaseModel) for item in get_args(field.annotation)):
|
|
94
|
+
# Get the BaseModel class from the Union
|
|
95
|
+
nested_model = next(item for item in get_args(field.annotation)
|
|
96
|
+
if isinstance(item, type) and issubclass(item, BaseModel))
|
|
97
|
+
nested[alias] = self._xsd_flat_to_nested(flat_dict, nested_model)
|
|
98
|
+
|
|
99
|
+
# Handle primitive fields - your logic + XSD value wrapping
|
|
100
|
+
else:
|
|
101
|
+
if key_in_input in flat_dict:
|
|
102
|
+
value = flat_dict[key_in_input]
|
|
103
|
+
|
|
104
|
+
# XSD enhancement: Check if this field type needs {"value": ...} wrapping
|
|
105
|
+
field_type = self._unwrap_optional_and_list(field.annotation)
|
|
106
|
+
if (hasattr(field_type, 'model_fields') and
|
|
107
|
+
'value' in getattr(field_type, 'model_fields', {})):
|
|
108
|
+
nested[alias] = {"value": value}
|
|
109
|
+
else:
|
|
110
|
+
nested[alias] = value
|
|
111
|
+
|
|
112
|
+
return nested
|
|
113
|
+
|
|
114
|
+
def _unwrap_optional_and_list(self, field_type):
|
|
115
|
+
"""Helper to unwrap Optional and List types to get the core type."""
|
|
116
|
+
# Handle Optional[T] (Union[T, None])
|
|
117
|
+
origin = get_origin(field_type)
|
|
118
|
+
if origin is type(Optional[str]) or str(origin) == 'typing.Union':
|
|
119
|
+
args = get_args(field_type)
|
|
120
|
+
if args:
|
|
121
|
+
for arg in args:
|
|
122
|
+
if arg is not type(None):
|
|
123
|
+
field_type = arg
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
# Handle List[T]
|
|
127
|
+
if get_origin(field_type) is list:
|
|
128
|
+
args = get_args(field_type)
|
|
129
|
+
if args:
|
|
130
|
+
field_type = args[0]
|
|
131
|
+
|
|
132
|
+
return field_type
|