brynq-sdk-zenegy 1.3.3__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 (33) hide show
  1. brynq_sdk_zenegy/__init__.py +23 -0
  2. brynq_sdk_zenegy/absence.py +100 -0
  3. brynq_sdk_zenegy/companies.py +43 -0
  4. brynq_sdk_zenegy/cost_center.py +80 -0
  5. brynq_sdk_zenegy/departments.py +77 -0
  6. brynq_sdk_zenegy/employee_documents.py +40 -0
  7. brynq_sdk_zenegy/employees.py +301 -0
  8. brynq_sdk_zenegy/global_value_sets.py +260 -0
  9. brynq_sdk_zenegy/global_values.py +265 -0
  10. brynq_sdk_zenegy/paychecks.py +119 -0
  11. brynq_sdk_zenegy/payroll.py +117 -0
  12. brynq_sdk_zenegy/payslips.py +43 -0
  13. brynq_sdk_zenegy/pensions.py +118 -0
  14. brynq_sdk_zenegy/schemas/__init__.py +30 -0
  15. brynq_sdk_zenegy/schemas/absences.py +393 -0
  16. brynq_sdk_zenegy/schemas/companies.py +42 -0
  17. brynq_sdk_zenegy/schemas/company_cost_centers.py +48 -0
  18. brynq_sdk_zenegy/schemas/company_departments.py +147 -0
  19. brynq_sdk_zenegy/schemas/employee_documents.py +30 -0
  20. brynq_sdk_zenegy/schemas/employee_pay_checks.py +169 -0
  21. brynq_sdk_zenegy/schemas/employee_pensions.py +140 -0
  22. brynq_sdk_zenegy/schemas/employees.py +2372 -0
  23. brynq_sdk_zenegy/schemas/global_value_sets.py +185 -0
  24. brynq_sdk_zenegy/schemas/global_values.py +433 -0
  25. brynq_sdk_zenegy/schemas/payrolls.py +134 -0
  26. brynq_sdk_zenegy/schemas/payslips.py +32 -0
  27. brynq_sdk_zenegy/schemas/supplements_and_deductions_rates.py +189 -0
  28. brynq_sdk_zenegy/supplements_and_deductions_rates.py +71 -0
  29. brynq_sdk_zenegy/zenegy.py +221 -0
  30. brynq_sdk_zenegy-1.3.3.dist-info/METADATA +16 -0
  31. brynq_sdk_zenegy-1.3.3.dist-info/RECORD +33 -0
  32. brynq_sdk_zenegy-1.3.3.dist-info/WHEEL +5 -0
  33. brynq_sdk_zenegy-1.3.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,23 @@
1
+ """
2
+ Brynq SDK for Zenegy API integration
3
+ """
4
+
5
+ from .absence import Absences
6
+ from .companies import Companies
7
+ from .cost_center import CostCenter
8
+ from .departments import CompanyDepartments
9
+ from .employees import Employees
10
+ from .employee_documents import EmployeeDocuments
11
+ from .global_values import GlobalValues
12
+ from .global_value_sets import GlobalValueSets
13
+ from .paychecks import PayChecks
14
+ from .payroll import Payrolls
15
+ from .payslips import Payslips
16
+ from .pensions import Pensions
17
+ from .supplements_and_deductions_rates import SupplementsAndDeductionsRates
18
+
19
+ __all__ = [
20
+ 'Absences', 'Companies', 'CostCenter', 'CompanyDepartments', 'Employees',
21
+ 'EmployeeDocuments', 'GlobalValues', 'GlobalValueSets', 'PayChecks', 'Payrolls',
22
+ 'Payslips', 'Pensions', 'SupplementsAndDeductionsRates'
23
+ ]
@@ -0,0 +1,100 @@
1
+ from .schemas.absences import (CreateAbsenceRequest,
2
+ UpdateAbsenceRequest,
3
+ AbsenceGet)
4
+ import requests
5
+ from uuid import UUID
6
+ from brynq_sdk_functions import Functions
7
+ from typing import Dict, Any, List, Tuple
8
+ import pandas as pd
9
+
10
+ class Absences:
11
+ """
12
+ Handles all absence-related operations in Zenegy API
13
+ """
14
+
15
+ def __init__(self, zenegy):
16
+ """
17
+ Initialize the Absences class.
18
+
19
+ Args:
20
+ zenegy: The Zenegy instance to use for API calls
21
+ """
22
+ self.zenegy = zenegy
23
+ self.endpoint = f"api/companies/{self.zenegy.company_uid}/absence"
24
+
25
+ def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
26
+ """
27
+ GetAbsenceDaysPerCompany
28
+ Returns:
29
+ DataFrame with absence information
30
+ """
31
+ endpoint = f"api/companies/{self.zenegy.company_uid}/absence"
32
+ try:
33
+ # Make the API request and get raw response
34
+ content = self.zenegy.get(endpoint=endpoint)
35
+
36
+ # Get data from response
37
+ data = content.get("data", [])
38
+ if data:
39
+ # Normalize the data
40
+ df = pd.json_normalize(
41
+ data,
42
+ sep='__'
43
+ )
44
+ if df.empty:
45
+ return pd.DataFrame(), pd.DataFrame()
46
+ # Validate data using schema
47
+ valid_data, invalid_data = Functions.validate_data(df, AbsenceGet)
48
+ return valid_data, invalid_data
49
+ return pd.DataFrame(), pd.DataFrame()
50
+ except Exception as e:
51
+ raise Exception(f"Failed to retrieve absences: {str(e)}") from e
52
+
53
+ def create(self, data: Dict[str, Any]) -> requests.Response:
54
+ """
55
+ Create
56
+ Args:
57
+ data (Dict[str, Any]): The data
58
+ Returns:
59
+ requests.Response: The API response
60
+ """
61
+ # Validate the data using Pydantic
62
+ try:
63
+ valid_data = CreateAbsenceRequest(**data)
64
+ req_body = valid_data.model_dump(by_alias=True, mode='json',exclude_none=True)
65
+ response = self.zenegy.post(endpoint=self.endpoint, json=req_body)
66
+ self.zenegy.raise_for_status_with_details(response)
67
+ return response
68
+ except Exception as e:
69
+ raise Exception(f"Failed to create absence: {str(e)}")
70
+
71
+ def delete(self, absence_uid: UUID) -> requests.Response:
72
+ endpoint = f"{self.endpoint}/{absence_uid}"
73
+ try:
74
+ response = self.zenegy.delete(endpoint=endpoint)
75
+ self.zenegy.raise_for_status_with_details(response)
76
+ return response
77
+ except Exception as e:
78
+ raise Exception(f"Failed to delete absence: {str(e)}")
79
+
80
+ def update(self, absence_uid: UUID, data: Dict[str, Any]) -> requests.Response:
81
+ """
82
+ Update an existing absence record.
83
+
84
+ Args:
85
+ absence_uid (UUID): The absence uid to update
86
+ data (Dict[str, Any]): The updated absence data
87
+
88
+ Returns:
89
+ requests.Response: The response from the API
90
+ """
91
+ # Validate the data using Pydantic
92
+ try:
93
+ valida_data = UpdateAbsenceRequest(**data)
94
+ req_body = valida_data.model_dump(by_alias=True, mode='json', exclude_none=True)
95
+ endpoint = f"{self.endpoint}/{absence_uid}"
96
+ response = self.zenegy.put(endpoint=endpoint, json=req_body)
97
+ self.zenegy.raise_for_status_with_details(response)
98
+ return response
99
+ except Exception as e:
100
+ raise Exception(f"Failed to update absences: {str(e)}")
@@ -0,0 +1,43 @@
1
+ from .schemas.companies import CompaniesGet
2
+ from typing import Tuple
3
+ import pandas as pd
4
+ from brynq_sdk_functions import Functions
5
+
6
+ class Companies:
7
+ """
8
+ Handles all company-related operations in Zenegy API
9
+ """
10
+
11
+ def __init__(self, zenegy):
12
+ """
13
+ Initialize the Companies class.
14
+
15
+ Args:
16
+ zenegy: The Zenegy instance to use for API calls
17
+ """
18
+ self.zenegy = zenegy
19
+ self.endpoint = "api/companies"
20
+
21
+ def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
22
+ """
23
+ GetCurrentUserCompanies
24
+
25
+ Returns:
26
+ Tuple of (valid_data, invalid_data) DataFrames with company information
27
+ """
28
+ try:
29
+ # Make the API request and get raw response
30
+ content = self.zenegy.get(endpoint=self.endpoint)
31
+
32
+ # Normalize the data (content is already an array)
33
+ df = pd.DataFrame(content)
34
+
35
+ if df.empty:
36
+ return pd.DataFrame(), pd.DataFrame()
37
+
38
+ # Validate data using schema
39
+ valid_data, invalid_data = Functions.validate_data(df, CompaniesGet)
40
+ return valid_data, invalid_data
41
+
42
+ except Exception as e:
43
+ raise Exception(f"Failed to retrieve companies: {str(e)}") from e
@@ -0,0 +1,80 @@
1
+ import requests
2
+ from uuid import UUID
3
+ from .schemas.company_cost_centers import (CostCenterCreate,
4
+ CostCentersGet)
5
+ from brynq_sdk_functions import Functions
6
+ from typing import Dict, Any, List, Tuple
7
+ import pandas as pd
8
+
9
+
10
+ class CostCenter:
11
+ """
12
+ Handles all company cost center related operations in Zenegy API
13
+ """
14
+
15
+ def __init__(self, zenegy):
16
+ """
17
+ Initialize the Companycostcenters class.
18
+
19
+ Args:
20
+ zenegy: The Zenegy instance to use for API calls
21
+ """
22
+ self.zenegy = zenegy
23
+ self.endpoint = f"api/companies/{self.zenegy.company_uid}/cost-center"
24
+
25
+ def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
26
+ """
27
+ GetCompanyCostCentersAsync
28
+ Returns:
29
+ Tuple of (valid_data, invalid_data) DataFrames with cost center information
30
+ """
31
+ try:
32
+ # Make the API request and get raw response
33
+ content = self.zenegy.get(endpoint=self.endpoint)
34
+
35
+ # Normalize the data (content is already an array)
36
+ df = pd.DataFrame(content)
37
+
38
+ if df.empty:
39
+ return pd.DataFrame(), pd.DataFrame()
40
+
41
+ # Validate data using schema
42
+ valid_data, invalid_data = Functions.validate_data(df, CostCentersGet)
43
+ return valid_data, invalid_data
44
+
45
+ except Exception as e:
46
+ raise Exception(f"Failed to retrieve cost centers: {str(e)}") from e
47
+
48
+ def create(self, data: Dict[str, Any]) -> requests.Response:
49
+ """
50
+ CreateCostCenterAsync
51
+ Args:
52
+ data (Dict[str, Any]): The data
53
+ Returns:
54
+ requests.Response: The API response
55
+ """
56
+ # Validate the data using Pydantic
57
+ try:
58
+ req_data = CostCenterCreate(**data)
59
+ req_body = req_data.model_dump(by_alias=True, mode='json',exclude_none=True)
60
+ response = self.zenegy.post(endpoint=self.endpoint, json=req_body)
61
+ self.zenegy.raise_for_status_with_details(response)
62
+ return response
63
+ except Exception as e:
64
+ raise Exception(f"Failed to create cost center: {str(e)}")
65
+
66
+ def delete(self, cost_center_uid: UUID) -> requests.Response:
67
+ """
68
+ DeleteCostCenterAsync
69
+ Args:
70
+ cost_center_uid (UUID): The cost center uid
71
+ Returns:
72
+ requests.Response: The API response
73
+ """
74
+ endpoint = f"{self.endpoint}/{cost_center_uid}"
75
+ try:
76
+ response = self.zenegy.delete(endpoint=endpoint)
77
+ self.zenegy.raise_for_status_with_details(response)
78
+ return response
79
+ except Exception as e:
80
+ raise Exception(f"Failed to delete cost center: {str(e)}")
@@ -0,0 +1,77 @@
1
+ from uuid import UUID
2
+ from .schemas.company_departments import DepartmentsGet
3
+ from typing import Tuple
4
+ import pandas as pd
5
+ from brynq_sdk_functions import Functions
6
+
7
+
8
+ class CompanyDepartments:
9
+ """
10
+ Handles all companydepartment-related operations in Zenegy API
11
+ """
12
+
13
+ def __init__(self, zenegy):
14
+ """
15
+ Initialize the CompanyDepartments class.
16
+
17
+ Args:
18
+ zenegy: The Zenegy instance to use for API calls
19
+ """
20
+ self.zenegy = zenegy
21
+ self.endpoint = f"api/companies/{self.zenegy.company_uid}/departments"
22
+
23
+ def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
24
+ """
25
+ GetCompanyDepartmentsAsync
26
+ Returns:
27
+ Tuple of (valid_data, invalid_data) DataFrames with department information
28
+ """
29
+ try:
30
+ # Make the API request and get raw response
31
+ content = self.zenegy.get(endpoint=self.endpoint)
32
+
33
+ # Get data from response
34
+ data = content.get("data", [])
35
+
36
+ df = pd.json_normalize(
37
+ data,
38
+ sep='__'
39
+ )
40
+
41
+ if df.empty:
42
+ return pd.DataFrame(), pd.DataFrame()
43
+
44
+ # Validate data using schema
45
+ valid_data, invalid_data = Functions.validate_data(df, DepartmentsGet)
46
+ return valid_data, invalid_data
47
+
48
+ except Exception as e:
49
+ raise Exception(f"Failed to retrieve departments: {str(e)}") from e
50
+
51
+ def get_by_id(self, company_department_uid: UUID) -> Tuple[pd.DataFrame, pd.DataFrame]:
52
+ """
53
+ GetCompanyDepartment
54
+ Args:
55
+ company_department_uid (UUID): The company department uid
56
+ Returns:
57
+ Tuple of (valid_data, invalid_data) DataFrames with department information
58
+ """
59
+ try:
60
+ endpoint = f"{self.endpoint}/{company_department_uid}"
61
+ # Make the API request and get raw response
62
+ content = self.zenegy.get(endpoint=endpoint)
63
+
64
+ df = pd.json_normalize(
65
+ content,
66
+ sep='__'
67
+ )
68
+
69
+ if df.empty:
70
+ return pd.DataFrame(), pd.DataFrame()
71
+
72
+ # Validate data using schema
73
+ valid_data, invalid_data = Functions.validate_data(df, DepartmentsGet)
74
+ return valid_data, invalid_data
75
+
76
+ except Exception as e:
77
+ raise Exception(f"Failed to retrieve department by ID: {str(e)}") from e
@@ -0,0 +1,40 @@
1
+ from uuid import UUID
2
+ from typing import Tuple
3
+ import pandas as pd
4
+
5
+ from brynq_sdk_functions import Functions
6
+ from .schemas.employee_documents import EmployeeDocumentsGet
7
+
8
+
9
+ class EmployeeDocuments:
10
+ """Handles all employee document related operations in the Zenegy API"""
11
+
12
+ def __init__(self, zenegy):
13
+ """Initialize the EmployeeDocuments class with a Zenegy client instance."""
14
+ self.zenegy = zenegy
15
+
16
+ def get(self, employee_uid: UUID) -> Tuple[pd.DataFrame, pd.DataFrame]:
17
+ """
18
+ Retrieve all documents for a given employee.
19
+
20
+ Args:
21
+ employee_uid: The employee UID.
22
+
23
+ Returns:
24
+ Tuple of (valid_data, invalid_data) Pandas DataFrames.
25
+ """
26
+ try:
27
+ endpoint = (
28
+ f"api/employees/{employee_uid}/companies/{self.zenegy.company_uid}/documents"
29
+ )
30
+ content = self.zenegy.get(endpoint=endpoint)
31
+
32
+ records = content if isinstance(content, list) else content.get("data", [])
33
+ df = pd.json_normalize(records, sep="__")
34
+ if df.empty:
35
+ return pd.DataFrame(), pd.DataFrame()
36
+
37
+ valid_data, invalid_data = Functions.validate_data(df, EmployeeDocumentsGet)
38
+ return valid_data, invalid_data
39
+ except Exception as e:
40
+ raise Exception(f"Failed to retrieve employee documents: {str(e)}") from e
@@ -0,0 +1,301 @@
1
+ # Generated endpoint class for tag: Employees
2
+ import pandas as pd
3
+ import requests
4
+ from uuid import UUID
5
+ from brynq_sdk_functions import Functions
6
+
7
+ from .schemas.employees import (EmployeeCreate,
8
+ EmployeeUpdate,
9
+ EmployeesGet,
10
+ EmployeesGetById,
11
+ EmployeeEmploymentDataUpdate,
12
+ EmployeeEmploymentUpdate,
13
+ EmployeeAdditionalUpdate,
14
+ StartSaldo,
15
+ EmployeePatch)
16
+ from .paychecks import PayChecks
17
+ from .pensions import Pensions
18
+ from typing import Dict, Any, Tuple, List
19
+
20
+
21
+ class Employees:
22
+ """
23
+ Handles all employees-related operations in Zenegy API
24
+ """
25
+
26
+ def __init__(self, zenegy):
27
+ """
28
+ Initialize the Employees class.
29
+
30
+ Args:
31
+ zenegy: The Zenegy instance to use for API calls
32
+ """
33
+ self.zenegy = zenegy
34
+ self.endpoint = f"api/companies/{self.zenegy.company_uid}/employees"
35
+
36
+ # Initialize paychecks and pensions
37
+ self.paychecks = PayChecks(zenegy)
38
+ self.pensions = Pensions(zenegy)
39
+
40
+ def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
41
+ """
42
+ GetEmployeeBasesAsync
43
+ Returns:
44
+ Tuple of (valid_data, invalid_data) DataFrames with employee information
45
+ """
46
+ try:
47
+ # Make the API request and get raw response
48
+ content = self.zenegy.get(endpoint=self.endpoint)
49
+
50
+ # Get data from response
51
+ data = content.get("data", [])
52
+
53
+ # Normalize the data
54
+ df = pd.json_normalize(
55
+ data,
56
+ sep='__'
57
+ )
58
+
59
+ if df.empty:
60
+ return pd.DataFrame(), pd.DataFrame()
61
+
62
+ # Validate data using schema
63
+ valid_data, invalid_data = Functions.validate_data(df, EmployeesGet)
64
+ return valid_data, invalid_data
65
+
66
+ except Exception as e:
67
+ raise Exception(f"Failed to retrieve employees: {str(e)}") from e
68
+
69
+ def get_by_id(self, employee_uid: UUID) -> Tuple[pd.DataFrame, pd.DataFrame]:
70
+ """
71
+ GetEmployeeAsync
72
+
73
+ Args:
74
+ employee_uid (UUID): The employee uid
75
+ Returns:
76
+ Tuple of (valid_data, invalid_data) DataFrames with employee information
77
+ """
78
+ endpoint = f"{self.endpoint}/{employee_uid}"
79
+ try:
80
+ # Make the API request and get raw response
81
+ content = self.zenegy.get(endpoint=endpoint)
82
+
83
+ # Normalize the data (content is already a dict)
84
+ df = pd.json_normalize(
85
+ [content], # Wrap single object in list for normalization
86
+ sep='__'
87
+ )
88
+
89
+ if df.empty:
90
+ return pd.DataFrame(), pd.DataFrame()
91
+
92
+ # Validate data using detailed by-id schema
93
+ valid_data, invalid_data = Functions.validate_data(df, EmployeesGetById)
94
+ return valid_data, invalid_data
95
+
96
+ except Exception as e:
97
+ raise Exception(f"Failed to retrieve employee by ID: {str(e)}") from e
98
+
99
+ def create(self, data: Dict[str, Any]) -> requests.Response:
100
+ """
101
+ PostEmployeeAsync
102
+ Args:
103
+ data (Dict[str, Any]): The data
104
+ Returns:
105
+ requests.Response: The API response
106
+ """
107
+ try:
108
+ req_data = EmployeeCreate(**data)
109
+ req_body = req_data.model_dump(by_alias=True, mode='json', exclude_none=True)
110
+ response = self.zenegy.post(endpoint=self.endpoint.lstrip('/'), json=req_body)
111
+ patch_body = self.create_update_body(data)
112
+
113
+ # if the patch body is bigger than the request body, that means that there are fields left for the patch:
114
+ # Count actual fields at the lowest level for proper comparison
115
+ patch_field_count = self._count_nested_fields(patch_body)
116
+ if patch_field_count > len(req_body):
117
+ response_data = response.json()
118
+ if isinstance(response_data, str):
119
+ uid = response_data
120
+ elif isinstance(response_data, dict):
121
+ uid = response_data['data']['uid']
122
+ patch_endpoint = f"{self.endpoint}/{uid}"
123
+
124
+ response = self.zenegy.put(
125
+ endpoint=patch_endpoint.lstrip('/'),
126
+ json=patch_body
127
+ )
128
+ self.zenegy.raise_for_status_with_details(response)
129
+ return response
130
+
131
+ self.zenegy.raise_for_status_with_details(response)
132
+ return response
133
+ except Exception as e:
134
+ raise Exception(f"Failed to create employee: {str(e)}")
135
+
136
+ def upsert(self, data: Dict[str, Any]) -> requests.Response:
137
+ """
138
+ UpsertEmployeeAsync
139
+
140
+ Args:
141
+ data (Dict[str, Any]): The data to update
142
+
143
+ Returns:
144
+ requests.Response: The API response
145
+ """
146
+ endpoint = f"api/companies/{self.zenegy.company_uid}/employees"
147
+ try:
148
+ req_body = self.create_update_body(data)
149
+ uid = data.get('employee_uid')
150
+ endpoint = f"{self.endpoint}/{uid}".lstrip('/')
151
+ response = self.zenegy.put(endpoint=endpoint, json=req_body)
152
+ self.zenegy.raise_for_status_with_details(response)
153
+ return response
154
+ except Exception as e:
155
+ raise Exception(f"Failed to update employee: {str(e)}")
156
+
157
+ def delete(self, employee_uid: UUID) -> requests.Response:
158
+ """
159
+ DeleteEmployee
160
+
161
+ Args:
162
+ employee_uid (UUID): The employee uid
163
+ Returns:
164
+ requests.Response: The API response
165
+ """
166
+ endpoint = f"{self.endpoint}/{employee_uid}"
167
+ try:
168
+ response = self.zenegy.delete(endpoint=endpoint)
169
+ self.zenegy.raise_for_status_with_details(response)
170
+ return response
171
+ except Exception as e:
172
+ raise Exception(f"Failed to delete employee: {str(e)}")
173
+
174
+ def create_update_body(self, data: Dict[str, Any]) -> Dict[str, Any]:
175
+ """
176
+ Create the update body for the employee.
177
+ """
178
+ # There is strange logic in Zenegy that you can create an employee with only a limited set of fields. Afterward, you have to patch the employee with the rest of the fields.
179
+ update_data = EmployeeUpdate(**data)
180
+ body = update_data.model_dump(by_alias=True, mode='json', exclude_none=True)
181
+
182
+
183
+ #-- START TODO: This is a temporary solution to handle the additional fields for the contract related fields in mft.
184
+ #additional fields for contract related fields
185
+ schema_map = {
186
+ "startSaldo": StartSaldo,
187
+ "employmentData": EmployeeEmploymentDataUpdate,
188
+ "employeeEmployment": EmployeeEmploymentUpdate,
189
+ "employeeAditional": EmployeeAdditionalUpdate
190
+ }
191
+
192
+ result_body = {"updateEmployeeBase": body}
193
+
194
+ # Initialize required top-level fields with empty objects (API requires these to always be present)
195
+ result_body["startSaldo"] = {} # can be passed empty
196
+ result_body["employeeEmployment"] = {}
197
+ result_body["employeeAditional"] = {"monthlySalaryFixedBase": 0}
198
+
199
+ # Check if any fields from data match schema fields, and serialize accordingly
200
+ for schema_name, schema_class in schema_map.items():
201
+ schema_fields = schema_class.model_fields.keys()
202
+ # Find matching keys between data and schema fields
203
+ matching_data = {k: v for k, v in data.items() if k in schema_fields}
204
+
205
+ # Skip if employee_number is the only field present
206
+ if matching_data: # and not (len(matching_data) == 1 and 'employee_number' in matching_data):
207
+ # Serialize the matching data using the schema
208
+ schema_instance = schema_class(**matching_data)
209
+ serialized_data = schema_instance.model_dump(by_alias=True, mode='json', exclude_none=True)
210
+
211
+ # startSaldo appears both inside updateEmployeeBase and at top level
212
+ if schema_name == "startSaldo":
213
+ result_body["updateEmployeeBase"][schema_name] = serialized_data
214
+ result_body[schema_name] = serialized_data
215
+ # employmentData appears inside updateEmployeeBase
216
+ elif schema_name == "employmentData":
217
+ result_body["updateEmployeeBase"][schema_name] = serialized_data
218
+ # employeeEmployment and employeeAditional appear at top level
219
+ else:
220
+ result_body[schema_name] = serialized_data
221
+ #--END TODO
222
+ return result_body
223
+
224
+ def _count_nested_fields(self, data: Dict[str, Any]) -> int:
225
+ """
226
+ Count the actual fields at the lowest level of a nested dictionary.
227
+ https://www.google.com/search?client=firefox-b-d&sca_esv=4acce884baa46368&sxsrf=AE3TifPbAKp8zW8Gczemzqd3WYBgKrcwlg:1761129017606&q=recursion&spell=1&sa=X&ved=2ahUKEwja3v3rzLeQAxUwwAIHHZJVKZUQBSgAegQIFhAB
228
+
229
+ Args:
230
+ data: Dictionary that may contain nested dictionaries
231
+
232
+ Returns:
233
+ int: Total count of fields at the lowest level
234
+ """
235
+ count = 0
236
+ for value in data.values():
237
+ if isinstance(value, dict):
238
+ count += self._count_nested_fields(value)
239
+ else:
240
+ count += 1
241
+ return count
242
+
243
+ def patch(self, employee_uid: UUID, data: Dict[str, Any], op: str = "replace") -> requests.Response:
244
+ """
245
+ PatchEmployee
246
+
247
+ Single entry point for patching employees using a flat data dictionary.
248
+ Flat keys may include prefixes for nested fields using a single underscore '_',
249
+ e.g., 'start_saldo_start_g_days', 'language_name'. All generated operations
250
+ are sent in ONE PATCH request as a JSON array per endpoint capability.
251
+
252
+ Args:
253
+ employee_uid (UUID): The employee uid
254
+ data (Dict[str, Any]): Flat dictionary with EmployeeUpdate fields (supports '_' prefix nesting in keys)
255
+ op (str): Operation type; defaults to "replace"
256
+
257
+ Returns:
258
+ requests.Response: Single response from the batch PATCH request
259
+ """
260
+ try:
261
+ if not op:
262
+ raise ValueError("Patch operation 'op' must be provided")
263
+
264
+ operations = self.build_patch_operations(data=data, op=op)
265
+ endpoint = f"{self.endpoint}/{employee_uid}".lstrip('/')
266
+ response = self.zenegy.patch(endpoint=endpoint, json=operations)
267
+ self.zenegy.raise_for_status_with_details(response)
268
+ return response
269
+ except Exception as e:
270
+ raise Exception(f"Failed to patch employee: {str(e)}")
271
+
272
+
273
+ def build_patch_operations(self, data: Dict[str, Any], op: str = "replace") -> List[Dict[str, Any]]:
274
+ """
275
+ Build JSON Patch operations from a flat employee data dictionary using
276
+ the EmployeePatch flat schema (aliases map directly to JSON Patch paths).
277
+
278
+ Args:
279
+ data (Dict[str, Any]): Flat data with pythonic keys (e.g., name, email, start_g_days)
280
+ op (str): JSON Patch op. Defaults to "replace".
281
+
282
+ Returns:
283
+ List[Dict[str, Any]]: JSON Patch operations
284
+ """
285
+ if not isinstance(data, dict):
286
+ raise ValueError("data must be a dictionary")
287
+ if not op:
288
+ raise ValueError("Patch operation 'op' must be provided")
289
+
290
+ # Validate against flat schema and dump using alias names
291
+ validated = EmployeePatch(**data)
292
+ alias_dump: Dict[str, Any] = validated.model_dump(by_alias=True, mode='json', exclude_none=True, exclude_unset=True)
293
+
294
+ operations: List[Dict[str, Any]] = []
295
+ for alias_key, value in alias_dump.items():
296
+ operations.append({
297
+ "op": op,
298
+ "path": f"/{alias_key}",
299
+ "value": value,
300
+ })
301
+ return operations