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,90 @@
1
+ from typing import Dict, Any, Optional, Tuple
2
+ import requests
3
+ import pandas as pd
4
+ from brynq_sdk_functions import Functions
5
+ from .schemas.agreement import AgreementRemunerationBankAccountUpdate
6
+ from .schemas.bank_account import AgreementBankAccountsGet
7
+ from typing import TYPE_CHECKING
8
+ if TYPE_CHECKING:
9
+ from .acerta import Acerta
10
+
11
+ class BankAccounts:
12
+ """Resource class for Employee Bank Accounts (HAL links)."""
13
+
14
+ def __init__(self, acerta):
15
+ self.acerta: Acerta = acerta
16
+ self.base_uri = "v1"
17
+
18
+ def get(self, agreement_id: Optional[str] = None, from_date: Optional[str] = None, until_date: Optional[str] = None, full_segments_only: Optional[bool] = None, accept_language: str = "en") -> Tuple[pd.DataFrame, pd.DataFrame]:
19
+ """
20
+ GET /v3/agreements/{agreementId}/remuneration - derive bank accounts
21
+
22
+ Fetches remuneration segments for agreements and returns only the bank account related fields
23
+ (via methodOfPayment.employeeBankAccounts/employerBankAccounts).
24
+ """
25
+ # Ensure we have agreement ids
26
+ if not agreement_id and not self.acerta._agreement_ids:
27
+ self.acerta.agreements.get()
28
+ agreement_ids = [agreement_id] if agreement_id else self.acerta._agreement_ids
29
+
30
+ params = {k: v for k, v in {
31
+ "fromDate": from_date,
32
+ "untilDate": until_date,
33
+ "fullSegmentsOnly": str(full_segments_only).lower() if isinstance(full_segments_only, bool) else full_segments_only,
34
+ }.items() if v is not None}
35
+
36
+ frames = []
37
+ headers = {"Accept-Language": accept_language} if accept_language else None
38
+ for agr_id in agreement_ids:
39
+ request_headers = self.acerta.session.headers.copy()
40
+ if headers:
41
+ request_headers.update(headers)
42
+ response = self.acerta.session.get(
43
+ url=f"{self.acerta.base_url}/agreement-data-management/v3/agreements/{agr_id}/remuneration",
44
+ params=params,
45
+ headers=request_headers,
46
+ timeout=self.acerta.TIMEOUT,
47
+ )
48
+ response.raise_for_status()
49
+ raw = response.json()
50
+ df = pd.json_normalize(
51
+ raw,
52
+ record_path=["remunerationSegments"],
53
+ meta=["agreementId"],
54
+ errors="ignore",
55
+ sep=".",
56
+ )
57
+ frames.append(df)
58
+
59
+ combined = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
60
+ return Functions.validate_data(combined, AgreementBankAccountsGet, debug=self.acerta.debug)
61
+
62
+ def update(self, agreement_id: str, data: Dict[str, Any]) -> requests.Response:
63
+ """
64
+ PUT /employee-data-management/v3/employees/agreement/{agreementId}/remuneration-bank-account - Agreement Remuneration Bank Account
65
+
66
+ Set the bank account for the employee and agreement. This endpoint updates the remuneration
67
+ bank account associated with a specific agreement.
68
+
69
+ Args:
70
+ agreement_id: Unique identifier of an agreement
71
+ data: Dictionary with bank account data (iban, bic)
72
+
73
+ Returns:
74
+ requests.Response: Raw response object
75
+
76
+ Raises:
77
+ Exception: If the update fails
78
+ """
79
+ # Validate the data (no nesting required for this simple schema)
80
+ validated_data = AgreementRemunerationBankAccountUpdate(**data)
81
+
82
+ # Make API request
83
+ response = self.acerta.session.put(
84
+ url=f"{self.acerta.base_url}/employee-data-management/v3/employees/agreement/{agreement_id}/remuneration-bank-account",
85
+ json=validated_data.model_dump(by_alias=True, exclude_none=True),
86
+ timeout=self.acerta.TIMEOUT,
87
+ )
88
+ response.raise_for_status()
89
+
90
+ return response
@@ -0,0 +1,264 @@
1
+ from typing import List, Tuple
2
+ import pandas as pd
3
+ import requests
4
+ from brynq_sdk_functions import Functions
5
+ from .schemas.planning import CountryGet, BelgianCityGet
6
+ from enum import Enum
7
+ from typing import List
8
+ from typing import TYPE_CHECKING
9
+ if TYPE_CHECKING:
10
+ from .acerta import Acerta
11
+
12
+ class CodeListAttribute(str, Enum):
13
+ ACE_CountryNisCode = 'ACE_CountryNisCode'
14
+ ACE_Gender = 'ACE_Gender'
15
+ ACE_Language = 'ACE_Language'
16
+ ACE_MaritalStatus = 'ACE_MaritalStatus'
17
+ ACE_Nationality = 'ACE_Nationality'
18
+ ACE_OfficialLanguageCode = 'ACE_OfficialLanguageCode'
19
+ ACE_RelationshipContact = 'ACE_RelationshipContact'
20
+ B10_AddressType = 'B10_AddressType'
21
+ B10_EducationalDegree = 'B10_EducationalDegree'
22
+ B10_LeadershipLevel = 'B10_LeadershipLevel'
23
+ B11_JointCommittee = 'B11_JointCommittee'
24
+ B11_PrefixNOSS = 'B11_PrefixNOSS'
25
+ B11_Qualification = 'B11_Qualification'
26
+ B11_PayFrequency = 'B11_PayFrequency'
27
+ B12_ApprenticeType = 'B12_ApprenticeType'
28
+ B12_CommunityCode = 'B12_CommunityCode'
29
+ B12_Customized_vacation_pay = 'B12_Customized_vacation_pay'
30
+ B12_Declaration_withholding_tax = 'B12_Declaration_withholding_tax '
31
+ B12_DMFA_Risc_labour_accident = 'B12_DMFA_Risc_labour_accident'
32
+ B12_Duration = 'B12_Duration'
33
+ B12_EarlyRetirementCode = 'B12_EarlyRetirementCode'
34
+ B12_EmployeeType = 'B12_EmployeeType'
35
+ B12_EmployeeTypeDet = 'B12_EmployeeTypeDet'
36
+ B12_ExemptionWorkPerformance = 'B12_ExemptionWorkPerformance'
37
+ B12_ExemptPartTimeCreditCode = 'B12_ExemptPartTimeCreditCode'
38
+ B12_Frequency_transport = 'B12_Frequency_transport'
39
+ B12_Functioncode = 'B12_Functioncode'
40
+ B12_HomeworkTransport = 'B12_HomeworkTransport'
41
+ B12_Involuntary_parttime = 'B12_Involuntary_parttime'
42
+ B12_Measure = 'B12_Measure'
43
+ B12_MeasureResumptionWorkCode = 'B12_MeasureResumptionWorkCode'
44
+ B12_Notion_NOSS = 'B12_Notion_NOSS'
45
+ B12_NotionDebtorCode = 'B12_NotionDebtorCode'
46
+ B12_Payment = 'B12_Payment'
47
+ B12_Reason_end_employment = 'B12_Reason_end_employment'
48
+ B12_Regularity = 'B12_Regularity'
49
+ B12_Salary_calculation = 'B12_Salary_calculation'
50
+ B12_Salary_split = 'B12_Salary_split'
51
+ B12_SalaryScaleTypeCode = 'B12_SalaryScaleTypeCode'
52
+ B12_Tariff_period = 'B12_Tariff_period'
53
+ B12_TimeCredit = 'B12_TimeCredit'
54
+ B12_TipsCode = 'B12_TipsCode'
55
+ B12_TipsTypeCode = 'B12_TipsTypeCode'
56
+ B12_Transport_type = 'B12_Transport_type'
57
+ B12_Type_of_subscription = 'B12_Type_of_subscription'
58
+ B12_Type_withholding_tax = 'B12_Type_withholding_tax'
59
+ B12_TypeAgreement = 'B12_TypeAgreement'
60
+ B12_WorkscheduleType = 'B12_WorkscheduleType'
61
+ BA4_BelgianPostalCode = 'BA4_BelgianPostalCode'
62
+ B12_MeasureNonProfit = 'B12_MeasureNonProfit'
63
+ B12_FunctionNOSS = 'B12_FunctionNOSS'
64
+ B12_PensionSystem = 'B12_PensionSystem'
65
+ B12_NACE = 'B12_NACE'
66
+ B12_Capelo_PersonnelCategory = 'B12_Capelo_PersonnelCategory'
67
+ B12_Capelo_Function = 'B12_Capelo_Function'
68
+ B12_Capelo_InstitutionType = 'B12_Capelo_InstitutionType'
69
+ B12_Capelo_ServiceType = 'B12_Capelo_ServiceType'
70
+ B12_SickLeaveReserve = 'B12_SickLeaveReserve'
71
+ B12_Wage_system = 'B12_Wage_system'
72
+ B12_Diploma = 'B12_Diploma'
73
+ B12_ParticularCompetence = 'B12_ParticularCompetence'
74
+ B12_DistinguishingLetter = 'B12_DistinguishingLetter'
75
+ B12_ServiceActivity = 'B12_ServiceActivity'
76
+ B12_StatusHospital = 'B12_StatusHospital'
77
+ B12_AdditionalKwalification = 'B12_AdditionalKwalification'
78
+ B12_Specialism = 'B12_Specialism'
79
+ B12_StaffTypeHospital = 'B12_StaffTypeHospital'
80
+ B12_FinancingCode = 'B12_FinancingCode'
81
+ B12_KwalificationEmployee = 'B12_KwalificationEmployee'
82
+ B12_OccupationalClassMWB = 'B12_OccupationalClassMWB'
83
+ B12_KwalificationSupervisor = 'B12_KwalificationSupervisor'
84
+ B12_IntensitySupervision = 'B12_IntensitySupervision'
85
+ B12_StatusTargetGroup = 'B12_StatusTargetGroup'
86
+ B12_Regional_recruitment_framework = 'B12_Regional_recruitment_framework'
87
+ B12_Level = 'B12_Level'
88
+ B12_ProgWorkResumpType = 'B12_ProgWorkResumpType'
89
+ B11_VIA = 'B11_VIA'
90
+ B12_SocialMaribel = 'B12_SocialMaribel'
91
+ B12_Tax_Statute = 'B12_Tax_Statute'
92
+ B12_Frontier_worker = 'B12_Frontier_worker'
93
+ B12_AidZone = 'B12_AidZone'
94
+ B12_Other_Social_Services = 'B12_Other_Social_Services'
95
+ B12_Restructuring_difficulties = 'B12_Restructuring_difficulties'
96
+ B12_Start_Job = 'B12_Start_Job'
97
+ B12_CareerMeasure = 'B12_CareerMeasure'
98
+ B12_Employer_recruitment_framework = 'B12_Employer_recruitment_framework'
99
+ B12_Employee_recruitment_framework = 'B12_Employee_recruitment_framework'
100
+ B12_IFICFunctionCode = 'B12_IFICFunctionCode'
101
+ B12_WageCategory = 'B12_WageCategory'
102
+ B12_RizivFrequencyCode = 'B12_RizivFrequencyCode'
103
+ B12_RizivCode = 'B12_RizivCode'
104
+ B12_PercentageCode = 'B12_PercentageCode'
105
+ B12_Employee_type_2 = 'B12_Employee_type_2'
106
+ B12_Statute = 'B12_Statute'
107
+ B12_Employee_type = 'B12_Employee_type'
108
+ B12_Sort = 'B12_Sort'
109
+ ACE_YesNoUnknown = 'ACE_YesNoUnknown'
110
+
111
+
112
+ class CodeLists:
113
+ """Resource class for Code Lists endpoints"""
114
+
115
+ def __init__(self, acerta):
116
+ self.acerta: Acerta = acerta
117
+ self.base_uri = "v1"
118
+ self.attributes = CodeListAttribute
119
+
120
+ def get_countries(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
121
+ """
122
+ GET /v1/countries - Countries
123
+
124
+ Retrieves the list of all countries with their NIS codes and descriptions.
125
+
126
+ Returns:
127
+ Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after validation
128
+
129
+ Raises:
130
+ Exception: If the retrieval fails
131
+ """
132
+ # Make API request
133
+ response = self.acerta.session.get(
134
+ url=f"{self.acerta.base_url}/employee-data-management/v1/countries",
135
+ timeout=self.acerta.TIMEOUT,
136
+ )
137
+ response.raise_for_status()
138
+ data = response.json()
139
+
140
+ # Normalize data
141
+ if isinstance(data, list):
142
+ df = pd.json_normalize(data)
143
+ else:
144
+ df = pd.DataFrame([data])
145
+
146
+ # Validate with schema
147
+ valid_data, invalid_data = Functions.validate_data(df, CountryGet)
148
+
149
+ return valid_data, invalid_data
150
+
151
+ def get_belgian_cities(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
152
+ """
153
+ GET /v1/belgian-cities - Belgian Cities
154
+
155
+ Retrieves the list of all Belgian cities with their postal codes, names, and NIS codes.
156
+
157
+ Returns:
158
+ Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after validation
159
+
160
+ Raises:
161
+ Exception: If the retrieval fails
162
+ """
163
+ # Make API request
164
+ response = self.acerta.session.get(
165
+ url=f"{self.acerta.base_url}/employee-data-management/v1/belgian-cities",
166
+ timeout=self.acerta.TIMEOUT,
167
+ )
168
+ response.raise_for_status()
169
+ data = response.json()
170
+
171
+ # Normalize data
172
+ if isinstance(data, list):
173
+ df = pd.json_normalize(data)
174
+ else:
175
+ df = pd.DataFrame([data])
176
+
177
+ # Validate with schema
178
+ valid_data, invalid_data = Functions.validate_data(df, BelgianCityGet)
179
+
180
+ return valid_data, invalid_data
181
+
182
+ def get_application_translations(self, app_id: str, lang: str, _format: str = "json"):
183
+ """
184
+ GET /labelcms/v1/applications/{app_id}/translations - Application labels
185
+
186
+ Retrieves application labels/translations for specified languages.
187
+
188
+ Args:
189
+ app_id: Application ID
190
+ lang: Comma separated list of languages (e.g., "EN,NL,FR")
191
+ _format: Request format (default: "json")
192
+
193
+ Returns:
194
+ Raw JSON response from API
195
+
196
+ Raises:
197
+ Exception: If the retrieval fails
198
+ """
199
+ params = {
200
+ "lang": lang,
201
+ "_format": _format
202
+ }
203
+ response = self.acerta.session.get(
204
+ url=f"{self.acerta.base_url}/labelcms/v1/applications/{app_id}/translations",
205
+ params=params,
206
+ timeout=self.acerta.TIMEOUT,
207
+ )
208
+ response.raise_for_status()
209
+ return response.json()
210
+
211
+
212
+ def get_cod_lists(
213
+ self,
214
+ attributes: List[CodeListAttribute],
215
+ lang: str = 'en,nl'
216
+ ):
217
+ """
218
+ GET /labelcms/v1/translations - Attributes collection (COD lists)
219
+
220
+ Retrieves generic COD lists applicable within Acerta (country codes, gender, fuel types, etc.).
221
+ These lists are mainly used to facilitate updates and creations of resources (Employee, Agreement, etc.).
222
+
223
+ Args:
224
+ attributes: List of COD list enum values or names (e.g., [CodListAttribute.B11_Fuel, CodListAttribute.B11_Gender]).
225
+ lang: Comma separated list of languages (e.g., "en,nl,fr,de")
226
+ _format: Request format (default: "json")
227
+
228
+ Returns:
229
+ Raw JSON response from API containing possible values for specified attributes and languages
230
+
231
+ Raises:
232
+ Exception: If the retrieval fails
233
+
234
+ Example:
235
+ response = acerta.planning.get_cod_lists(
236
+ attributes=[CodListAttribute.B11_Fuel, CodListAttribute.B11_Gender],
237
+ lang="en,nl"
238
+ )
239
+ """
240
+ # Accept string literals as fallback but enforce they match enum
241
+ valid_values = set(item.value for item in attributes)
242
+ attribute_strings = []
243
+ for attribute in attributes:
244
+ if isinstance(attribute, CodeListAttribute):
245
+ attribute_strings.append(attribute.value)
246
+ elif isinstance(attribute, str) and attribute in valid_values:
247
+ attribute_strings.append(attribute)
248
+ else:
249
+ raise ValueError(
250
+ f"Invalid attribute: {attribute}. Possible values: {[a.value for a in attributes]}"
251
+ )
252
+
253
+ attribute_string = ",".join(attribute_strings)
254
+ params = {
255
+ "lang": lang,
256
+ "attributes": attribute_string,
257
+ "_format": "json"
258
+ }
259
+ response = requests.get(
260
+ url=f"https://labelcms.acerta.be/en/v1/translations",
261
+ params=params
262
+ )
263
+ response.raise_for_status()
264
+ return response.json()
@@ -0,0 +1,135 @@
1
+ from typing import Tuple, Dict, Any, Optional
2
+ import pandas as pd
3
+ import requests
4
+ from brynq_sdk_functions import Functions
5
+ from .schemas.company_car import CompanyCarGet, CompanyCarCreate, CompanyCarUpdate
6
+ from typing import TYPE_CHECKING
7
+ if TYPE_CHECKING:
8
+ from .acerta import Acerta
9
+
10
+
11
+ class CompanyCars:
12
+ """Resource class for Company Cars endpoints"""
13
+
14
+ def __init__(self, acerta):
15
+ self.acerta: Acerta = acerta
16
+ self.base_uri = "employer-data-management/v1/employers"
17
+
18
+ def get(
19
+ self,
20
+ from_date: str = "1900-01-01",
21
+ until_date: str = "9999-12-31",
22
+ license_plate: Optional[str] = None,
23
+ size: int = 20,
24
+ page: int = 0,
25
+ ) -> Tuple[pd.DataFrame, pd.DataFrame]:
26
+ """
27
+ GET /v1/employers/{employerId}/company-cars - Company cars
28
+
29
+ Retrieve company cars for all configured employer IDs within an optional time window
30
+ and optional license plate filter. Supports basic pagination parameters.
31
+
32
+ Args:
33
+ from_date: The lower bound of the time window (default: "1900-01-01")
34
+ until_date: The upper bound of the time window (default: "9999-12-31")
35
+ license_plate: Optional license plate filter (or part of a plate)
36
+ size: Number of items per page (default: 20)
37
+ page: Zero-based page index (default: 0)
38
+
39
+ Returns:
40
+ Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after normalizing and validating
41
+ """
42
+ all_valid_data = []
43
+ all_invalid_data = []
44
+ for employer_id in self.acerta._employer_ids:
45
+ params = {
46
+ "fromDate": from_date,
47
+ "untilDate": until_date,
48
+ "size": size,
49
+ "page": page,
50
+ }
51
+ if license_plate:
52
+ params["licensePlate"] = license_plate
53
+ response = self.acerta.session.get(
54
+ url=f"{self.acerta.base_url}/{self.base_uri}/{employer_id}/company-cars",
55
+ params=params,
56
+ timeout=self.acerta.TIMEOUT,
57
+ )
58
+ response.raise_for_status()
59
+ items = response.json().get("companyCars", [])
60
+ df = pd.json_normalize(items, sep='.')
61
+ valid_data, invalid_data = Functions.validate_data(df, CompanyCarGet)
62
+ all_valid_data.append(valid_data)
63
+ all_invalid_data.append(invalid_data)
64
+
65
+ return (
66
+ pd.concat(all_valid_data, ignore_index=True) if all_valid_data else pd.DataFrame(),
67
+ pd.concat(all_invalid_data, ignore_index=True) if all_invalid_data else pd.DataFrame(),
68
+ )
69
+
70
+ def get_by_id(self, employer_id: str, company_car_id: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
71
+ """
72
+ GET /v1/employers/{employerId}/company-cars/{companyCarId} - Company car by ID
73
+
74
+ Args:
75
+ employer_id: Unique identifier of the employer (Acerta key)
76
+ company_car_id: Unique identifier of the company car (6 digits)
77
+
78
+ Returns:
79
+ Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df)
80
+ """
81
+ response = self.acerta.session.get(
82
+ url=f"{self.acerta.base_url}/{self.base_uri}/{employer_id}/company-cars/{company_car_id}",
83
+ timeout=self.acerta.TIMEOUT,
84
+ )
85
+ response.raise_for_status()
86
+ raw = response.json()
87
+ if isinstance(raw, dict):
88
+ df = pd.json_normalize(raw, sep='.')
89
+ else:
90
+ df = pd.json_normalize([raw], sep='.')
91
+ return Functions.validate_data(df, CompanyCarGet)
92
+
93
+ def create(self, employer_id: str, data: Dict[str, Any]) -> requests.Response:
94
+ """
95
+ POST /v1/employers/{employerId}/company-cars - Company car
96
+
97
+ Args:
98
+ employer_id: Unique identifier of the employer (Acerta key)
99
+ data: Flat dictionary with company car data
100
+
101
+ Returns:
102
+ requests.Response: Raw response object
103
+ """
104
+ # Convert flat data to nested using Functions.flat_to_nested_with_prefix
105
+ nested = Functions.flat_to_nested_with_prefix(data, CompanyCarCreate)
106
+ validated = CompanyCarCreate(**nested)
107
+ response = self.acerta.session.post(
108
+ url=f"{self.acerta.base_url}/{self.base_uri}/{employer_id}/company-cars",
109
+ json=validated.model_dump(by_alias=True, exclude_none=True),
110
+ timeout=self.acerta.TIMEOUT,
111
+ )
112
+ response.raise_for_status()
113
+ return response
114
+
115
+ def update(self, employer_id: str, company_car_id: str, data: Dict[str, Any]) -> requests.Response:
116
+ """
117
+ PATCH /v1/employers/{employerId}/company-cars/{companyCarId} - Update company car
118
+
119
+ Args:
120
+ employer_id: Unique identifier of the employer (Acerta key)
121
+ company_car_id: Unique identifier of the company car
122
+ data: Flat dictionary with update fields
123
+
124
+ Returns:
125
+ requests.Response: Raw response object
126
+ """
127
+ nested = Functions.flat_to_nested_with_prefix(data, CompanyCarUpdate)
128
+ validated = CompanyCarUpdate(**nested)
129
+ response = self.acerta.session.patch(
130
+ url=f"{self.acerta.base_url}/{self.base_uri}/{employer_id}/company-cars/{company_car_id}",
131
+ json=validated.model_dump(by_alias=True, exclude_none=True),
132
+ timeout=self.acerta.TIMEOUT,
133
+ )
134
+ response.raise_for_status()
135
+ return response
@@ -0,0 +1,79 @@
1
+ from typing import Dict, Any, Tuple
2
+ import pandas as pd
3
+ import requests
4
+ from brynq_sdk_functions import Functions
5
+ from .schemas.contact_information import ContactInformationGet, ContactInformationUpdate
6
+ from typing import TYPE_CHECKING
7
+ if TYPE_CHECKING:
8
+ from .acerta import Acerta
9
+
10
+
11
+ class ContactInformation:
12
+ """Resource class for Employee Contact 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 /employee-data-management/v3/employees/{employeeId}/contact-information - Employee Contact Information
21
+
22
+ Retrieves employee contact information for all cached employees including personal contact (phone, mobile, email),
23
+ work contact, and emergency contacts (primary and secondary).
24
+
25
+ Args:
26
+
27
+ Returns:
28
+ Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after normalizing and validating
29
+ """
30
+ if not self.acerta._employee_ids:
31
+ self.acerta.agreements.get()
32
+
33
+ frames = []
34
+ for employee_id in self.acerta._employee_ids:
35
+ response = self.acerta.session.get(
36
+ url=f"{self.acerta.base_url}/{self.base_uri}/{employee_id}/contact-information",
37
+ timeout=self.acerta.TIMEOUT,
38
+ )
39
+ response.raise_for_status()
40
+ df = pd.json_normalize(response.json())
41
+ if 'externalReferences' in df.columns:
42
+ df = df.drop(columns=['externalReferences'])
43
+ frames.append(df)
44
+
45
+ combined = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
46
+
47
+ valid_df, invalid_df = Functions.validate_data(combined, ContactInformationGet)
48
+
49
+ return valid_df, invalid_df
50
+
51
+ def update(self, employee_id: str, data: Dict[str, Any]) -> requests.Response:
52
+ """
53
+ PATCH /employee-data-management/v3/employees/{employeeId}/contact-information - Employee Contact Information
54
+
55
+ Update employee contact information including personal contact (phone, mobile, email),
56
+ work contact, and emergency contacts (primary and secondary).
57
+
58
+ Args:
59
+ employee_id: Unique identifier of an employee
60
+ data: Flat dictionary with contact information data
61
+
62
+ Returns:
63
+ requests.Response: Raw response object
64
+ """
65
+ # Convert flat data to nested using Functions.flat_to_nested_with_prefix
66
+ nested_data = Functions.flat_to_nested_with_prefix(data, ContactInformationUpdate)
67
+
68
+ # Validate the nested data
69
+ validated_data = ContactInformationUpdate(**nested_data)
70
+
71
+ # Make API request
72
+ response = self.acerta.session.patch(
73
+ url=f"{self.acerta.base_url}/{self.base_uri}/{employee_id}/contact-information",
74
+ json=validated_data.model_dump(by_alias=True, exclude_none=True),
75
+ timeout=self.acerta.TIMEOUT,
76
+ )
77
+ response.raise_for_status()
78
+
79
+ return response
@@ -0,0 +1,94 @@
1
+ from typing import Tuple, Dict, Any
2
+ import pandas as pd
3
+ import requests
4
+ from brynq_sdk_functions import Functions
5
+ from .schemas.cost_center import CostCenterGet, CostCenterCreate
6
+ from typing import TYPE_CHECKING
7
+ if TYPE_CHECKING:
8
+ from .acerta import Acerta
9
+
10
+
11
+ class CostCenters:
12
+ """Resource class for Cost Centers endpoints"""
13
+
14
+ def __init__(self, acerta):
15
+ self.acerta: Acerta = acerta
16
+ self.base_uri = "employer-data-management/v1/employers"
17
+
18
+ def get(self, from_date: str = "1900-01-01", until_date: str = "9999-12-31",
19
+ size: int = 20, page: int = 0) -> Tuple[pd.DataFrame, pd.DataFrame]:
20
+ """
21
+ GET /v1/employers/{employerId}/cost-centers - Cost centers
22
+
23
+ This endpoint returns the cost centers of an employer based on its employerId for a given period.
24
+ An employer can have 0 to N cost centers. Cost centers are primarily used for reporting purposes.
25
+
26
+ Args:
27
+ employer_id: Unique identifier of the employer (Acerta key)
28
+ from_date: The lower bound of the time window (default: "1900-01-01")
29
+ until_date: The upper bound of the time window (default: "9999-12-31")
30
+ size: Number of items to be returned on each page (default: 20)
31
+ page: Zero-based page index (default: 0)
32
+
33
+ Returns:
34
+ Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after normalizing and validating
35
+
36
+ Raises:
37
+ Exception: If the retrieval fails
38
+ """
39
+ all_valid_data = []
40
+ all_invalid_data = []
41
+ for employer_id in self.acerta._employer_ids:
42
+ # Prepare query parameters
43
+ params = {
44
+ "fromDate": from_date,
45
+ "untilDate": until_date,
46
+ "size": size,
47
+ "page": page
48
+ }
49
+ # Make API request
50
+ response = self.acerta.session.get(
51
+ url=f"{self.acerta.base_url}/{self.base_uri}/{employer_id}/cost-centers",
52
+ params=params,
53
+ timeout=self.acerta.TIMEOUT,
54
+ )
55
+ response.raise_for_status()
56
+ cost_centers_data = response.json().get("costCenters", [])
57
+ df = pd.json_normalize(cost_centers_data, sep='.')
58
+ valid_data, invalid_data = Functions.validate_data(df, CostCenterGet)
59
+ all_valid_data.append(valid_data)
60
+ all_invalid_data.append(invalid_data)
61
+
62
+ return pd.concat(all_valid_data, ignore_index=True), pd.concat(all_invalid_data, ignore_index=True)
63
+
64
+ def create(self, employer_id: str, data: Dict[str, Any]) -> requests.Response:
65
+ """
66
+ POST /v1/employers/{employerId}/cost-centers - Cost center
67
+
68
+ This endpoint creates a cost center for an employer.
69
+
70
+ Args:
71
+ employer_id: Unique identifier of the employer (Acerta key)
72
+ data: Flat dictionary with cost center data
73
+
74
+ Returns:
75
+ requests.Response: Raw response object
76
+
77
+ Raises:
78
+ Exception: If the creation fails
79
+ """
80
+ # Convert flat data to nested using Functions.flat_to_nested_with_prefix
81
+ nested_data = Functions.flat_to_nested_with_prefix(data, CostCenterCreate)
82
+
83
+ # Validate the nested data
84
+ validated_data = CostCenterCreate(**nested_data)
85
+
86
+ # Make API request
87
+ response = self.acerta.session.post(
88
+ url=f"{self.acerta.base_url}/{self.base_uri}/{employer_id}/cost-centers",
89
+ json=validated_data.model_dump(by_alias=True, exclude_none=True),
90
+ timeout=self.acerta.TIMEOUT,
91
+ )
92
+ response.raise_for_status()
93
+
94
+ return response