brynq-sdk-acerta 1.1.1__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_acerta/__init__.py +14 -0
- brynq_sdk_acerta/acerta.py +118 -0
- brynq_sdk_acerta/addresses.py +99 -0
- brynq_sdk_acerta/agreements.py +426 -0
- brynq_sdk_acerta/bank_accounts.py +90 -0
- brynq_sdk_acerta/code_lists.py +264 -0
- brynq_sdk_acerta/company_cars.py +135 -0
- brynq_sdk_acerta/contact_information.py +79 -0
- brynq_sdk_acerta/cost_centers.py +94 -0
- brynq_sdk_acerta/employees.py +121 -0
- brynq_sdk_acerta/employees_additional_information.py +87 -0
- brynq_sdk_acerta/employer.py +179 -0
- brynq_sdk_acerta/family_members.py +99 -0
- brynq_sdk_acerta/family_situation.py +99 -0
- brynq_sdk_acerta/inservice.py +99 -0
- brynq_sdk_acerta/salaries.py +74 -0
- brynq_sdk_acerta/schemas/__init__.py +135 -0
- brynq_sdk_acerta/schemas/address.py +80 -0
- brynq_sdk_acerta/schemas/agreement.py +982 -0
- brynq_sdk_acerta/schemas/bank_account.py +87 -0
- brynq_sdk_acerta/schemas/company_car.py +124 -0
- brynq_sdk_acerta/schemas/contact_information.py +83 -0
- brynq_sdk_acerta/schemas/cost_center.py +82 -0
- brynq_sdk_acerta/schemas/employee.py +406 -0
- brynq_sdk_acerta/schemas/employer.py +71 -0
- brynq_sdk_acerta/schemas/family.py +220 -0
- brynq_sdk_acerta/schemas/in_service.py +243 -0
- brynq_sdk_acerta/schemas/in_service_config.py +28 -0
- brynq_sdk_acerta/schemas/planning.py +37 -0
- brynq_sdk_acerta/schemas/salaries.py +84 -0
- brynq_sdk_acerta-1.1.1.dist-info/METADATA +21 -0
- brynq_sdk_acerta-1.1.1.dist-info/RECORD +34 -0
- brynq_sdk_acerta-1.1.1.dist-info/WHEEL +5 -0
- brynq_sdk_acerta-1.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Dict, Any, Tuple
|
|
2
|
+
import warnings
|
|
3
|
+
import requests
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from .family_members import FamilyMembers
|
|
6
|
+
from brynq_sdk_functions import Functions
|
|
7
|
+
from .schemas.employee import PersonalDetailsGet, PersonalDetailsUpdate, EmployeeCreate
|
|
8
|
+
from .employees_additional_information import EmployeesAdditionalInformation
|
|
9
|
+
from .addresses import Addresses
|
|
10
|
+
from .contact_information import ContactInformation
|
|
11
|
+
from .family_situation import FamilySituation
|
|
12
|
+
from .bank_accounts import BankAccounts
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .acerta import Acerta
|
|
16
|
+
|
|
17
|
+
class Employees:
|
|
18
|
+
"""Resource class for Employee endpoints"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, acerta):
|
|
21
|
+
self.acerta: Acerta = acerta
|
|
22
|
+
self.base_uri = "employee-data-management"
|
|
23
|
+
|
|
24
|
+
# Initialize subclass resources
|
|
25
|
+
self.addresses = Addresses(acerta)
|
|
26
|
+
self.contact_information = ContactInformation(acerta)
|
|
27
|
+
self.family_situation = FamilySituation(acerta)
|
|
28
|
+
self.bank_accounts = BankAccounts(acerta)
|
|
29
|
+
self.additional_information = EmployeesAdditionalInformation(acerta)
|
|
30
|
+
self.family_members = FamilyMembers(acerta)
|
|
31
|
+
|
|
32
|
+
def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
33
|
+
"""
|
|
34
|
+
GET /v2/employees/{employeeId} - Employee
|
|
35
|
+
|
|
36
|
+
Retrieves employee information including personal data, birth information, gender,
|
|
37
|
+
nationality, languages, and family situation. Address and contact information
|
|
38
|
+
are excluded (available in separate endpoints).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
employee_id: Employee identifier
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after normalizing and validating
|
|
45
|
+
"""
|
|
46
|
+
all_dfs = []
|
|
47
|
+
for employee_id in self.acerta._employee_ids:
|
|
48
|
+
response = self.acerta.session.get(
|
|
49
|
+
url=f"{self.acerta.base_url}/{self.base_uri}/v3/employees/{employee_id}/personal-details",
|
|
50
|
+
timeout=self.acerta.TIMEOUT,
|
|
51
|
+
)
|
|
52
|
+
response.raise_for_status()
|
|
53
|
+
df = pd.json_normalize(response.json())
|
|
54
|
+
all_dfs.append(df)
|
|
55
|
+
|
|
56
|
+
employees = pd.concat(all_dfs, ignore_index=True) if all_dfs else pd.DataFrame()
|
|
57
|
+
valid_employees, invalid_employees = Functions.validate_data(employees, PersonalDetailsGet)
|
|
58
|
+
|
|
59
|
+
return valid_employees, invalid_employees
|
|
60
|
+
|
|
61
|
+
def update(self, employee_id: str, data: Dict[str, Any]) -> requests.Response:
|
|
62
|
+
"""
|
|
63
|
+
PATCH /v3/employees/{employeeId}/personal-details - Employee personal details
|
|
64
|
+
|
|
65
|
+
Update employee personal details including name, birth information, nationality,
|
|
66
|
+
languages, identification numbers, and work permits.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
employee_id: Employee identifier
|
|
70
|
+
data: Flat dictionary with personal details data
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
requests.Response: Raw response object
|
|
74
|
+
"""
|
|
75
|
+
# Convert flat data to nested using Functions.flat_to_nested_with_prefix
|
|
76
|
+
nested_data = Functions.flat_to_nested_with_prefix(data, PersonalDetailsUpdate)
|
|
77
|
+
|
|
78
|
+
# Validate the nested data
|
|
79
|
+
validated_data = PersonalDetailsUpdate(**nested_data)
|
|
80
|
+
|
|
81
|
+
# Make API request
|
|
82
|
+
response = self.acerta.session.patch(
|
|
83
|
+
url=f"{self.acerta.base_url}/{self.base_uri}/v3/employees/{employee_id}/personal-details",
|
|
84
|
+
json=validated_data.model_dump(by_alias=True, exclude_none=True),
|
|
85
|
+
timeout=self.acerta.TIMEOUT,
|
|
86
|
+
)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
|
|
89
|
+
return response
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create(self, data: Dict[str, Any]) -> requests.Response:
|
|
94
|
+
"""
|
|
95
|
+
POST /v3/employees - Employee
|
|
96
|
+
|
|
97
|
+
Create an employee without an agreement. Includes personal details, addresses,
|
|
98
|
+
contact information, family situation, bank accounts, and additional information.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
data: Flat dictionary with employee data
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
requests.Response: Raw response object with employeeId
|
|
105
|
+
"""
|
|
106
|
+
warnings.warn("Do not use this endpoint if you need to add a contract. There is no endpoint to create contracts separately, it's only possible on in-service requests. This endpoint is only used if you want to create an employee without a contract.")
|
|
107
|
+
# Convert flat data to nested using Functions.flat_to_nested_with_prefix
|
|
108
|
+
nested_data = Functions.flat_to_nested_with_prefix(data, EmployeeCreate)
|
|
109
|
+
|
|
110
|
+
# Validate the nested data
|
|
111
|
+
validated_data = EmployeeCreate(**nested_data)
|
|
112
|
+
|
|
113
|
+
# Make API request
|
|
114
|
+
response = self.acerta.session.post(
|
|
115
|
+
url=f"{self.acerta.base_url}/{self.base_uri}/v3/employees",
|
|
116
|
+
json=validated_data.model_dump(by_alias=True, exclude_none=True),
|
|
117
|
+
timeout=self.acerta.TIMEOUT,
|
|
118
|
+
)
|
|
119
|
+
response.raise_for_status()
|
|
120
|
+
|
|
121
|
+
return response
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import Tuple, Dict, Any
|
|
2
|
+
import requests
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from brynq_sdk_functions import Functions
|
|
5
|
+
from .schemas.employee import AdditionalInformationGet, AdditionalInformationUpdate
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .acerta import Acerta
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EmployeesAdditionalInformation:
|
|
12
|
+
"""Resource class for Employee Additional Information endpoints"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, acerta):
|
|
15
|
+
self.acerta: Acerta = acerta
|
|
16
|
+
self.base_uri = "employee-data-management/v3/employees"
|
|
17
|
+
|
|
18
|
+
def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
19
|
+
"""
|
|
20
|
+
GET /v3/employees/{employeeId}/additional-information - Employee Additional Information
|
|
21
|
+
|
|
22
|
+
Retrieves employee additional information including educational degree and leadership level
|
|
23
|
+
for all cached employees. This data is historical (segmented by periods).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after normalizing and validating
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
if not self.acerta._employee_ids:
|
|
30
|
+
self.acerta.agreements.get()
|
|
31
|
+
|
|
32
|
+
frames = []
|
|
33
|
+
for employee_id in self.acerta._employee_ids:
|
|
34
|
+
response = self.acerta.session.get(
|
|
35
|
+
url=f"{self.acerta.base_url}/{self.base_uri}/{employee_id}/additional-information",
|
|
36
|
+
timeout=self.acerta.TIMEOUT,
|
|
37
|
+
)
|
|
38
|
+
response.raise_for_status()
|
|
39
|
+
content = response.json()
|
|
40
|
+
df = pd.json_normalize(
|
|
41
|
+
content,
|
|
42
|
+
record_path=["additionalInformationSegments"],
|
|
43
|
+
meta=["employeeId"],
|
|
44
|
+
sep="."
|
|
45
|
+
)
|
|
46
|
+
external_ref_columns = [col for col in df.columns if "externalReferences" in col]
|
|
47
|
+
if external_ref_columns:
|
|
48
|
+
df = df.drop(columns=external_ref_columns)
|
|
49
|
+
frames.append(df)
|
|
50
|
+
|
|
51
|
+
combined = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
|
|
52
|
+
|
|
53
|
+
valid_df, invalid_df = Functions.validate_data(combined, AdditionalInformationGet)
|
|
54
|
+
|
|
55
|
+
return valid_df, invalid_df
|
|
56
|
+
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise Exception(f"Failed to retrieve employee additional information: {str(e)}") from e
|
|
59
|
+
|
|
60
|
+
def update(self, employee_id: str, data: Dict[str, Any]) -> requests.Response:
|
|
61
|
+
"""
|
|
62
|
+
PATCH /v3/employees/{employeeId}/additional-information - Employee Additional Information
|
|
63
|
+
|
|
64
|
+
Update employee additional information including educational degree and leadership level.
|
|
65
|
+
This data is historical.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
employee_id: Employee identifier
|
|
69
|
+
data: Dictionary with additional information data (fromDate, educationalDegree, leadershipLevel)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
requests.Response: Raw response object
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
validated_data = AdditionalInformationUpdate(**data)
|
|
76
|
+
|
|
77
|
+
response = self.acerta.session.patch(
|
|
78
|
+
url=f"{self.acerta.base_url}/{self.base_uri}/{employee_id}/additional-information",
|
|
79
|
+
json=validated_data.model_dump(by_alias=True, exclude_none=True),
|
|
80
|
+
timeout=self.acerta.TIMEOUT,
|
|
81
|
+
)
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
|
|
84
|
+
return response
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
raise Exception(f"Failed to update additional information: {str(e)}") from e
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from typing import Tuple, Optional, Literal
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from brynq_sdk_functions import Functions
|
|
4
|
+
from .schemas.employer import JointCommitteeGet, FunctionGet, SalaryCodeGet
|
|
5
|
+
from .cost_centers import CostCenters
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .acerta import Acerta
|
|
9
|
+
|
|
10
|
+
# Type aliases for employee service types
|
|
11
|
+
ServiceType = Literal[
|
|
12
|
+
'WHITE_COLLAR',
|
|
13
|
+
'LABOURER',
|
|
14
|
+
'BLUE_COLLAR',
|
|
15
|
+
'STUDENT_BLUE_COLLAR',
|
|
16
|
+
'STUDENT_WHITE_COLLAR',
|
|
17
|
+
'FLEX_BLUE_COLLAR',
|
|
18
|
+
'FLEX_WHITE_COLLAR'
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Employer:
|
|
23
|
+
"""Resource class for Employer endpoints"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, acerta):
|
|
26
|
+
self.acerta: Acerta = acerta
|
|
27
|
+
|
|
28
|
+
# Initialize subclass resources
|
|
29
|
+
self.cost_centers = CostCenters(acerta)
|
|
30
|
+
|
|
31
|
+
def get_joint_committees(self, employer_id: Optional[str] = None, in_service_type: Optional[ServiceType] = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
32
|
+
"""
|
|
33
|
+
GET /employers/{employerId}/joint-committees - Joint Committees
|
|
34
|
+
|
|
35
|
+
Retrieves all active joint committees for a specific employer. The output serves as input
|
|
36
|
+
for the employee onboarding process. Can be filtered by employee classification.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
employer_id: The ID for the employer for which the joint committees are requested
|
|
40
|
+
in_service_type: The type of employee for which the joint committees should be active (optional)
|
|
41
|
+
Options: WHITE_COLLAR, LABOURER, BLUE_COLLAR, STUDENT_BLUE_COLLAR,
|
|
42
|
+
STUDENT_WHITE_COLLAR, FLEX_BLUE_COLLAR, FLEX_WHITE_COLLAR
|
|
43
|
+
accept_language: Response language (default: "EN")
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after validation
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
Exception: If the retrieval fails
|
|
50
|
+
"""
|
|
51
|
+
employers = [employer_id] if employer_id else self.acerta._employer_ids
|
|
52
|
+
all_rows = []
|
|
53
|
+
for emp_id in employers:
|
|
54
|
+
params = {}
|
|
55
|
+
if in_service_type:
|
|
56
|
+
params['inServiceType'] = in_service_type
|
|
57
|
+
response = self.acerta.session.get(
|
|
58
|
+
url=f"{self.acerta.base_url}/employee-in-service-request/v1/employers/{emp_id}/joint-committees",
|
|
59
|
+
params=params,
|
|
60
|
+
timeout=self.acerta.TIMEOUT,
|
|
61
|
+
)
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
data = response.json()
|
|
64
|
+
joint_committees = data.get("_embedded", {}).get("jointCommittees", [])
|
|
65
|
+
rows = []
|
|
66
|
+
for jc in joint_committees:
|
|
67
|
+
base_data = {
|
|
68
|
+
"employerId": emp_id,
|
|
69
|
+
"code": jc.get("code"),
|
|
70
|
+
"description": jc.get("description")
|
|
71
|
+
}
|
|
72
|
+
links = jc.get("_links", [])
|
|
73
|
+
if links:
|
|
74
|
+
for link in links:
|
|
75
|
+
row = {**base_data}
|
|
76
|
+
row["_links.rel"] = link.get("rel")
|
|
77
|
+
row["_links.href"] = link.get("href")
|
|
78
|
+
row["_links.title"] = link.get("title")
|
|
79
|
+
row["_links.type"] = link.get("type")
|
|
80
|
+
row["_links.templated"] = link.get("templated")
|
|
81
|
+
rows.append(row)
|
|
82
|
+
else:
|
|
83
|
+
rows.append(base_data)
|
|
84
|
+
if rows:
|
|
85
|
+
all_rows.extend(rows)
|
|
86
|
+
df = pd.DataFrame(all_rows)
|
|
87
|
+
valid_data, invalid_data = Functions.validate_data(df, JointCommitteeGet)
|
|
88
|
+
return valid_data, invalid_data
|
|
89
|
+
|
|
90
|
+
def get_functions(self, employer_id: Optional[str] = None, from_date: str = "1900-01-01", until_date: str = "9999-12-31",
|
|
91
|
+
size: int = 20, page: int = 0) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
92
|
+
"""
|
|
93
|
+
GET /v1/employers/{employerId}/functions - Functions
|
|
94
|
+
|
|
95
|
+
Returns employer specific functions based on an employerId.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
employer_id: Unique identifier of the employer (Acerta key)
|
|
99
|
+
from_date: The lower bound of the time window (default: "1900-01-01")
|
|
100
|
+
until_date: The upper bound of the time window (default: "9999-12-31")
|
|
101
|
+
size: Number of items to be returned on each page (default: 20)
|
|
102
|
+
page: Zero-based page index (default: 0)
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after validation
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
Exception: If the retrieval fails
|
|
109
|
+
"""
|
|
110
|
+
employers = [employer_id] if employer_id else self.acerta._employer_ids
|
|
111
|
+
all_frames = []
|
|
112
|
+
for emp_id in employers:
|
|
113
|
+
params = {
|
|
114
|
+
"fromDate": from_date,
|
|
115
|
+
"untilDate": until_date,
|
|
116
|
+
"size": size,
|
|
117
|
+
"page": page
|
|
118
|
+
}
|
|
119
|
+
response = self.acerta.session.get(
|
|
120
|
+
url=f"{self.acerta.base_url}/employee-in-service-request/v1/employers/{emp_id}/functions",
|
|
121
|
+
params=params,
|
|
122
|
+
timeout=self.acerta.TIMEOUT,
|
|
123
|
+
)
|
|
124
|
+
response.raise_for_status()
|
|
125
|
+
data = response.json()
|
|
126
|
+
functions = data.get("functions", [])
|
|
127
|
+
if functions:
|
|
128
|
+
df = pd.json_normalize(functions, sep='.')
|
|
129
|
+
df["employerId"] = emp_id
|
|
130
|
+
all_frames.append(df)
|
|
131
|
+
combined = pd.concat(all_frames, ignore_index=True) if all_frames else pd.DataFrame()
|
|
132
|
+
valid_data, invalid_data = Functions.validate_data(combined, FunctionGet)
|
|
133
|
+
return valid_data, invalid_data
|
|
134
|
+
|
|
135
|
+
def get_salary_codes(self, employer_id: Optional[str] = None, from_date: str = "1900-01-01", until_date: str = "9999-12-31",
|
|
136
|
+
size: int = 20, page: int = 0) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
137
|
+
"""
|
|
138
|
+
GET /v1/employers/{employerId}/salary-codes - Salary codes
|
|
139
|
+
|
|
140
|
+
A salary code is an identifier used to categorize different types of earnings and deductions.
|
|
141
|
+
It exists in the salary elements of a basic salary. This endpoint returns the employer specific
|
|
142
|
+
salary codes based on an employerId.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
employer_id: Unique identifier of the employer (Acerta key)
|
|
146
|
+
from_date: The lower bound of the time window (default: "1900-01-01")
|
|
147
|
+
until_date: The upper bound of the time window (default: "9999-12-31")
|
|
148
|
+
size: Number of items to be returned on each page (default: 20)
|
|
149
|
+
page: Zero-based page index (default: 0)
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after normalizing and validating
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
Exception: If the retrieval fails
|
|
156
|
+
"""
|
|
157
|
+
employers = [employer_id] if employer_id else self.acerta._employer_ids
|
|
158
|
+
all_frames = []
|
|
159
|
+
for emp_id in employers:
|
|
160
|
+
params = {
|
|
161
|
+
"fromDate": from_date,
|
|
162
|
+
"untilDate": until_date,
|
|
163
|
+
"size": size,
|
|
164
|
+
"page": page
|
|
165
|
+
}
|
|
166
|
+
response = self.acerta.session.get(
|
|
167
|
+
url=f"{self.acerta.base_url}/employer-data-management/v1/employers/{emp_id}/salary-codes",
|
|
168
|
+
params=params,
|
|
169
|
+
timeout=self.acerta.TIMEOUT,
|
|
170
|
+
)
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
salary_codes_data = response.json().get("salaryCodes", [])
|
|
173
|
+
if salary_codes_data:
|
|
174
|
+
df = pd.json_normalize(salary_codes_data, sep='.')
|
|
175
|
+
df["employerId"] = emp_id
|
|
176
|
+
all_frames.append(df)
|
|
177
|
+
combined = pd.concat(all_frames, ignore_index=True) if all_frames else pd.DataFrame()
|
|
178
|
+
valid_data, invalid_data = Functions.validate_data(combined, SalaryCodeGet)
|
|
179
|
+
return valid_data, invalid_data
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Dict, Any, Tuple
|
|
2
|
+
import requests
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from .schemas.family import FamilyMemberGet, FamilySituationUpdate, FamilyMemberCreate, FamilySituationGet
|
|
5
|
+
from brynq_sdk_functions import Functions
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .acerta import Acerta
|
|
9
|
+
|
|
10
|
+
class FamilyMembers:
|
|
11
|
+
"""Resource class for Family endpoints"""
|
|
12
|
+
# According to Acerta this is hardly ever used, is not relevant for payroll.
|
|
13
|
+
|
|
14
|
+
def __init__(self, acerta):
|
|
15
|
+
self.acerta: Acerta = acerta
|
|
16
|
+
self.base_uri = "employee-data-management/v2/employees"
|
|
17
|
+
|
|
18
|
+
def get(self, employee_id: str, from_date: str = "1900-01-01", until_date: str = "9999-12-31",
|
|
19
|
+
full_segments_only: bool = False) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
20
|
+
"""
|
|
21
|
+
GET /v2/employees/{employeeId}/family-members - Employee Family Members
|
|
22
|
+
|
|
23
|
+
Retrieves family member information including relationship, personalia (name, birth, gender),
|
|
24
|
+
and other details (dependant status, disability, insurance). Data is segmented by periods.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
employee_id: Unique identifier for an employee
|
|
28
|
+
from_date: Lower bound of the time window (default: "1900-01-01")
|
|
29
|
+
until_date: Upper bound of the time window (default: "9999-12-31")
|
|
30
|
+
full_segments_only: If True, only return complete periods (default: False)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after normalizing and validating
|
|
34
|
+
"""
|
|
35
|
+
# Prepare query parameters
|
|
36
|
+
params = {
|
|
37
|
+
"fromDate": from_date,
|
|
38
|
+
"untilDate": until_date,
|
|
39
|
+
"fullSegmentsOnly": full_segments_only
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Make API request
|
|
43
|
+
response = self.acerta.session.get(
|
|
44
|
+
url=f"{self.acerta.base_url}/{self.base_uri}/{employee_id}/family-members",
|
|
45
|
+
params=params,
|
|
46
|
+
timeout=self.acerta.TIMEOUT,
|
|
47
|
+
)
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
data = response.json()
|
|
50
|
+
|
|
51
|
+
# Extract and normalize data using pd.json_normalize
|
|
52
|
+
df = pd.json_normalize(
|
|
53
|
+
data,
|
|
54
|
+
record_path=['familyMembersSegments', 'familyMembers'],
|
|
55
|
+
meta=[
|
|
56
|
+
'employeeId',
|
|
57
|
+
['familyMembersSegments', 'period', 'startDate'],
|
|
58
|
+
['familyMembersSegments', 'period', 'endDate'],
|
|
59
|
+
['externalReferences', 0, 'externalReferenceType'],
|
|
60
|
+
['externalReferences', 0, 'externalReferenceNumber'],
|
|
61
|
+
['externalReferences', 0, 'companyOrganisationNumber']
|
|
62
|
+
],
|
|
63
|
+
sep='.'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Validate with schema
|
|
67
|
+
valid_data, invalid_data = Functions.validate_data(df, FamilyMemberGet)
|
|
68
|
+
|
|
69
|
+
return valid_data, invalid_data
|
|
70
|
+
|
|
71
|
+
def create(self, employee_id: str, data: Dict[str, Any]) -> requests.Response:
|
|
72
|
+
"""
|
|
73
|
+
POST /v1/employees/{employeeId}/family-members - Employee Family Members
|
|
74
|
+
|
|
75
|
+
Create a new family member for an employee including relationship, personalia
|
|
76
|
+
(name, birth, gender), and other information (dependant status, disability, insurance).
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
employee_id: Unique identifier of an employee
|
|
80
|
+
data: Flat dictionary with family member data
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
requests.Response: Raw response object
|
|
84
|
+
"""
|
|
85
|
+
# Convert flat data to nested using Functions.flat_to_nested_with_prefix
|
|
86
|
+
nested_data = Functions.flat_to_nested_with_prefix(data, FamilyMemberCreate)
|
|
87
|
+
|
|
88
|
+
# Validate the nested data
|
|
89
|
+
validated_data = FamilyMemberCreate(**nested_data)
|
|
90
|
+
|
|
91
|
+
# Make API request
|
|
92
|
+
response = self.acerta.session.post(
|
|
93
|
+
url=f"{self.acerta.base_url}/{self.base_uri}/{employee_id}/family-members",
|
|
94
|
+
json=validated_data.model_dump(by_alias=True, exclude_none=True),
|
|
95
|
+
timeout=self.acerta.TIMEOUT,
|
|
96
|
+
)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
|
|
99
|
+
return response
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Dict, Any, Tuple
|
|
2
|
+
import requests
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from .schemas.family import FamilyMemberGet, FamilySituationUpdate, FamilyMemberCreate, FamilySituationGet
|
|
5
|
+
from brynq_sdk_functions import Functions
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .acerta import Acerta
|
|
9
|
+
|
|
10
|
+
class FamilySituation:
|
|
11
|
+
"""Resource class for Family endpoints"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, acerta):
|
|
14
|
+
self.acerta: Acerta = acerta
|
|
15
|
+
self.base_uri = "v2"
|
|
16
|
+
|
|
17
|
+
def update(self, employee_id: str, data: Dict[str, Any]) -> requests.Response:
|
|
18
|
+
"""
|
|
19
|
+
PATCH /employee-data-management/v3/employees/{employeeId}/family-situation - Employee Family Situation
|
|
20
|
+
|
|
21
|
+
Update employee family situation including civil status, partner information,
|
|
22
|
+
dependants (children, over 65, others), and fiscal details.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
employee_id: Unique identifier for an employee
|
|
26
|
+
data: Flat dictionary with family situation data
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
requests.Response: Raw response object
|
|
30
|
+
"""
|
|
31
|
+
# Convert flat data to nested using Functions.flat_to_nested_with_prefix
|
|
32
|
+
nested_data = Functions.flat_to_nested_with_prefix(data, FamilySituationUpdate)
|
|
33
|
+
|
|
34
|
+
# Validate the nested data
|
|
35
|
+
validated_data = FamilySituationUpdate(**nested_data)
|
|
36
|
+
|
|
37
|
+
# Make API request
|
|
38
|
+
response = self.acerta.session.patch(
|
|
39
|
+
url=f"{self.acerta.base_url}/employee-data-management/v3/employees/{employee_id}/family-situation",
|
|
40
|
+
json=validated_data.model_dump(by_alias=True, exclude_none=True),
|
|
41
|
+
timeout=self.acerta.TIMEOUT,
|
|
42
|
+
)
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
def get(self, employee_id: str, from_date: str = "1900-01-01", until_date: str = "9999-12-31",
|
|
48
|
+
full_segments_only: bool = False) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
49
|
+
"""
|
|
50
|
+
GET /employee-data-management/v3/employees/{employeeId}/family-situations - Employee Family Situations
|
|
51
|
+
|
|
52
|
+
Retrieve employee family situation history within a specified time window. Returns
|
|
53
|
+
civil status, partner information, dependants, and fiscal details with their validity periods.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
employee_id: Unique identifier for an employee
|
|
57
|
+
from_date: Start date of the time window (default: "1900-01-01")
|
|
58
|
+
until_date: End date of the time window (default: "9999-12-31")
|
|
59
|
+
full_segments_only: If True, only return complete periods (default: False)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after validation
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
Exception: If the retrieval fails
|
|
66
|
+
"""
|
|
67
|
+
# Prepare query parameters
|
|
68
|
+
params = {
|
|
69
|
+
"fromDate": from_date,
|
|
70
|
+
"untilDate": until_date,
|
|
71
|
+
"fullSegmentsOnly": full_segments_only
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Make API request
|
|
75
|
+
response = self.acerta.session.get(
|
|
76
|
+
url=f"{self.acerta.base_url}/employee-data-management/v3/employees/{employee_id}/family-situations",
|
|
77
|
+
params=params,
|
|
78
|
+
timeout=self.acerta.TIMEOUT,
|
|
79
|
+
)
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
data = response.json()
|
|
82
|
+
|
|
83
|
+
# Normalize family situation segments data
|
|
84
|
+
df = pd.json_normalize(
|
|
85
|
+
data,
|
|
86
|
+
record_path=['familySituationSegments'],
|
|
87
|
+
meta=['employeeId'],
|
|
88
|
+
sep='.'
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Drop externalReferences columns if they exist
|
|
92
|
+
external_ref_columns = [col for col in df.columns if 'externalReferences' in col]
|
|
93
|
+
if external_ref_columns:
|
|
94
|
+
df = df.drop(columns=external_ref_columns)
|
|
95
|
+
|
|
96
|
+
# Validate with schema
|
|
97
|
+
valid_data, invalid_data = Functions.validate_data(df, FamilySituationGet)
|
|
98
|
+
|
|
99
|
+
return valid_data, invalid_data
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Dict, Any
|
|
2
|
+
import requests
|
|
3
|
+
from brynq_sdk_functions import Functions
|
|
4
|
+
from .schemas.in_service import EmploymentCreate, EmploymentRehire
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .acerta import Acerta
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InService:
|
|
11
|
+
"""Unified resource for Acerta in-service (hire/rehire) requests.
|
|
12
|
+
|
|
13
|
+
- Accepts flat snake_case input and converts it to nested payloads
|
|
14
|
+
using Functions.flat_to_nested_with_prefix and Employment* schemas
|
|
15
|
+
- Posts to the correct employee-in-service-request endpoints
|
|
16
|
+
- Polls the asynchronous status and returns parsed status data
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, acerta):
|
|
20
|
+
self.acerta: Acerta = acerta
|
|
21
|
+
|
|
22
|
+
def hire(self, employer_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
23
|
+
"""
|
|
24
|
+
POST /employee-in-service-request/v1/employers/{employerId}/employments - New employee & agreement
|
|
25
|
+
|
|
26
|
+
Accepts flat snake_case data (preferred) or already nested data.
|
|
27
|
+
If flat, converts to nested structure, validates, posts, and polls.
|
|
28
|
+
Returns the parsed status dict.
|
|
29
|
+
"""
|
|
30
|
+
# 1) Support both flat and nested payloads
|
|
31
|
+
if isinstance(data.get("employee"), dict) and isinstance(data.get("employment"), dict):
|
|
32
|
+
nested_data = data
|
|
33
|
+
else:
|
|
34
|
+
nested_data = Functions.flat_to_nested_with_prefix(data, EmploymentCreate)
|
|
35
|
+
|
|
36
|
+
# 2) Validate against in_service schema
|
|
37
|
+
validated = EmploymentCreate(**nested_data)
|
|
38
|
+
|
|
39
|
+
# 3) Make API request
|
|
40
|
+
response = self.acerta.session.post(
|
|
41
|
+
url=f"{self.acerta.base_url}/employee-in-service-request/v1/employers/{employer_id}/employments",
|
|
42
|
+
json=validated.model_dump(by_alias=True, exclude_none=True),
|
|
43
|
+
timeout=self.acerta.TIMEOUT,
|
|
44
|
+
)
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
|
|
47
|
+
# 4) Poll status via Location header
|
|
48
|
+
location = response.headers.get("Location")
|
|
49
|
+
request_id = location.rsplit("/", 1)[-1] if location else ""
|
|
50
|
+
status = self._get_in_service_status(request_id)
|
|
51
|
+
return status
|
|
52
|
+
|
|
53
|
+
def rehire(self, employer_id: str, employee_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
POST /employee-in-service-request/v1/employers/{employerId}/employees/{employeeId}/employments - Existing employee
|
|
56
|
+
|
|
57
|
+
Accepts flat snake_case data (preferred) or already nested data.
|
|
58
|
+
If flat, converts to nested structure, validates, posts, and polls.
|
|
59
|
+
Returns the parsed status dict.
|
|
60
|
+
"""
|
|
61
|
+
# 1) Support both flat and nested payloads
|
|
62
|
+
if isinstance(data.get("employer"), dict) or isinstance(data.get("employment"), dict):
|
|
63
|
+
nested_data = data
|
|
64
|
+
else:
|
|
65
|
+
nested_data = Functions.flat_to_nested_with_prefix(data, EmploymentRehire)
|
|
66
|
+
|
|
67
|
+
# 2) Validate against in_service schema
|
|
68
|
+
validated = EmploymentRehire(**nested_data)
|
|
69
|
+
|
|
70
|
+
# 3) Make API request
|
|
71
|
+
response = self.acerta.session.post(
|
|
72
|
+
url=(
|
|
73
|
+
f"{self.acerta.base_url}/employee-in-service-request/v1/employers/{employer_id}/employees/{employee_id}/employments"
|
|
74
|
+
),
|
|
75
|
+
json=validated.model_dump(by_alias=True, exclude_none=True),
|
|
76
|
+
timeout=self.acerta.TIMEOUT,
|
|
77
|
+
)
|
|
78
|
+
response.raise_for_status()
|
|
79
|
+
|
|
80
|
+
# 4) Poll status via Location header
|
|
81
|
+
location = response.headers.get("Location")
|
|
82
|
+
request_id = location.rsplit("/", 1)[-1] if location else ""
|
|
83
|
+
status = self._get_in_service_status(request_id)
|
|
84
|
+
return status
|
|
85
|
+
|
|
86
|
+
def _get_in_service_status(self, request_id: str) -> Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
GET /employee-in-service-request/v1/status/{requestId} - Request status
|
|
89
|
+
|
|
90
|
+
Returns parsed JSON status response for the asynchronous in-service request.
|
|
91
|
+
"""
|
|
92
|
+
endpoint = f"employee-in-service-request/v1/status/{request_id}"
|
|
93
|
+
response = self.acerta.session.get(
|
|
94
|
+
url=f"{self.acerta.base_url}/{endpoint}",
|
|
95
|
+
timeout=self.acerta.TIMEOUT,
|
|
96
|
+
)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
data = response.json()
|
|
99
|
+
return data
|