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.
Files changed (34) hide show
  1. brynq_sdk_acerta/__init__.py +14 -0
  2. brynq_sdk_acerta/acerta.py +118 -0
  3. brynq_sdk_acerta/addresses.py +99 -0
  4. brynq_sdk_acerta/agreements.py +426 -0
  5. brynq_sdk_acerta/bank_accounts.py +90 -0
  6. brynq_sdk_acerta/code_lists.py +264 -0
  7. brynq_sdk_acerta/company_cars.py +135 -0
  8. brynq_sdk_acerta/contact_information.py +79 -0
  9. brynq_sdk_acerta/cost_centers.py +94 -0
  10. brynq_sdk_acerta/employees.py +121 -0
  11. brynq_sdk_acerta/employees_additional_information.py +87 -0
  12. brynq_sdk_acerta/employer.py +179 -0
  13. brynq_sdk_acerta/family_members.py +99 -0
  14. brynq_sdk_acerta/family_situation.py +99 -0
  15. brynq_sdk_acerta/inservice.py +99 -0
  16. brynq_sdk_acerta/salaries.py +74 -0
  17. brynq_sdk_acerta/schemas/__init__.py +135 -0
  18. brynq_sdk_acerta/schemas/address.py +80 -0
  19. brynq_sdk_acerta/schemas/agreement.py +982 -0
  20. brynq_sdk_acerta/schemas/bank_account.py +87 -0
  21. brynq_sdk_acerta/schemas/company_car.py +124 -0
  22. brynq_sdk_acerta/schemas/contact_information.py +83 -0
  23. brynq_sdk_acerta/schemas/cost_center.py +82 -0
  24. brynq_sdk_acerta/schemas/employee.py +406 -0
  25. brynq_sdk_acerta/schemas/employer.py +71 -0
  26. brynq_sdk_acerta/schemas/family.py +220 -0
  27. brynq_sdk_acerta/schemas/in_service.py +243 -0
  28. brynq_sdk_acerta/schemas/in_service_config.py +28 -0
  29. brynq_sdk_acerta/schemas/planning.py +37 -0
  30. brynq_sdk_acerta/schemas/salaries.py +84 -0
  31. brynq_sdk_acerta-1.1.1.dist-info/METADATA +21 -0
  32. brynq_sdk_acerta-1.1.1.dist-info/RECORD +34 -0
  33. brynq_sdk_acerta-1.1.1.dist-info/WHEEL +5 -0
  34. 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