brynq-sdk-nmbrs 2.3.1__py3-none-any.whl → 2.3.2.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. brynq_sdk_nmbrs/__init__.py +104 -84
  2. brynq_sdk_nmbrs/address.py +82 -2
  3. brynq_sdk_nmbrs/children.py +96 -61
  4. brynq_sdk_nmbrs/companies.py +45 -1
  5. brynq_sdk_nmbrs/costcenter.py +53 -8
  6. brynq_sdk_nmbrs/costunit.py +16 -4
  7. brynq_sdk_nmbrs/debtors.py +76 -2
  8. brynq_sdk_nmbrs/department.py +149 -28
  9. brynq_sdk_nmbrs/document.py +50 -0
  10. brynq_sdk_nmbrs/employee_wage_tax_settings.py +113 -0
  11. brynq_sdk_nmbrs/employees.py +119 -24
  12. brynq_sdk_nmbrs/employment.py +12 -4
  13. brynq_sdk_nmbrs/function.py +128 -2
  14. brynq_sdk_nmbrs/leave.py +105 -8
  15. brynq_sdk_nmbrs/salaries.py +78 -3
  16. brynq_sdk_nmbrs/schedules.py +77 -3
  17. brynq_sdk_nmbrs/schemas/address.py +30 -5
  18. brynq_sdk_nmbrs/schemas/bank.py +0 -2
  19. brynq_sdk_nmbrs/schemas/children.py +67 -0
  20. brynq_sdk_nmbrs/schemas/company.py +16 -0
  21. brynq_sdk_nmbrs/schemas/contracts.py +25 -11
  22. brynq_sdk_nmbrs/schemas/costcenter.py +57 -18
  23. brynq_sdk_nmbrs/schemas/costunit.py +0 -2
  24. brynq_sdk_nmbrs/schemas/days.py +0 -2
  25. brynq_sdk_nmbrs/schemas/debtor.py +23 -1
  26. brynq_sdk_nmbrs/schemas/department.py +41 -7
  27. brynq_sdk_nmbrs/schemas/document.py +13 -0
  28. brynq_sdk_nmbrs/schemas/employees.py +44 -38
  29. brynq_sdk_nmbrs/schemas/employment.py +10 -10
  30. brynq_sdk_nmbrs/schemas/function.py +34 -7
  31. brynq_sdk_nmbrs/schemas/hours.py +0 -4
  32. brynq_sdk_nmbrs/schemas/leave.py +12 -1
  33. brynq_sdk_nmbrs/schemas/manager.py +0 -3
  34. brynq_sdk_nmbrs/schemas/salary.py +37 -12
  35. brynq_sdk_nmbrs/schemas/schedules.py +49 -1
  36. brynq_sdk_nmbrs/schemas/social_insurance.py +39 -6
  37. brynq_sdk_nmbrs/schemas/wage_tax.py +68 -8
  38. brynq_sdk_nmbrs/schemas/wage_tax_settings.py +76 -0
  39. brynq_sdk_nmbrs/schemas/wagecomponents.py +0 -4
  40. brynq_sdk_nmbrs/social_insurance.py +81 -3
  41. brynq_sdk_nmbrs/wage_tax.py +105 -4
  42. {brynq_sdk_nmbrs-2.3.1.dist-info → brynq_sdk_nmbrs-2.3.2.dev0.dist-info}/METADATA +1 -1
  43. brynq_sdk_nmbrs-2.3.2.dev0.dist-info/RECORD +55 -0
  44. {brynq_sdk_nmbrs-2.3.1.dist-info → brynq_sdk_nmbrs-2.3.2.dev0.dist-info}/WHEEL +1 -1
  45. brynq_sdk_nmbrs-2.3.1.dist-info/RECORD +0 -50
  46. {brynq_sdk_nmbrs-2.3.1.dist-info → brynq_sdk_nmbrs-2.3.2.dev0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,20 @@
1
+ from typing import TYPE_CHECKING, Any, Dict
2
+
1
3
  import pandas as pd
2
4
  import requests
3
- from dateutil.utils import today
4
- from requests import HTTPError
5
- from typing import Dict, Any, TYPE_CHECKING
6
5
 
7
6
  from brynq_sdk_functions import Functions
8
- from .schemas.costcenter import CostcenterGet, EmployeeCostcenterGet
9
- from .schemas.costcenter import EmployeeCostcenterUpdate, EmployeeCostcenterDelete
10
- from .schemas.costcenter import CostcenterCreate, CostcenterUpdate, CostcenterDelete
7
+
8
+ from .schemas.costcenter import (
9
+ CostcenterCreate,
10
+ CostcenterGet,
11
+ CostCentersResponse,
12
+ CostcenterUpdate,
13
+ EmployeeCostcenterGet,
14
+ EmployeeCostCentersResponse,
15
+ EmployeeCostcenterUpdate,
16
+ )
17
+
11
18
  if TYPE_CHECKING:
12
19
  from brynq_sdk_nmbrs import Nmbrs
13
20
 
@@ -40,12 +47,40 @@ class EmployeeCostcenter:
40
47
  url=f"{self.nmbrs.base_url}companies/{company_id}/employees/costcenters",
41
48
  params=params)
42
49
  data = self.nmbrs.get_paginated_result(request)
50
+
51
+ # Parse and validate API response using Pydantic models
52
+ if not data:
53
+ return pd.DataFrame()
54
+
55
+ # Validate response structure with Pydantic
56
+ response = EmployeeCostCentersResponse(data=data)
57
+
58
+ # Serialize models to dicts using model_dump with by_alias=True
59
+ serialized_data = [item.model_dump(by_alias=True, mode='json') for item in response.data]
60
+
61
+ # Use json_normalize to flatten nested structure efficiently
43
62
  df = pd.json_normalize(
44
- data,
63
+ serialized_data,
45
64
  record_path='employeeCostCenters',
46
65
  meta=['employeeId']
47
66
  )
48
67
 
68
+ # Flatten nested costUnits object if it exists
69
+ if 'costUnits' in df.columns:
70
+ # Check if costUnits contains dict-like objects
71
+ cost_units_dicts = df['costUnits'].dropna()
72
+ if not cost_units_dicts.empty and isinstance(cost_units_dicts.iloc[0], dict):
73
+ cost_units_expanded = pd.json_normalize(cost_units_dicts)
74
+ cost_units_expanded.columns = [f'costUnits.{col}' for col in cost_units_expanded.columns]
75
+ # Reindex to match original DataFrame index and fill missing values with None
76
+ cost_units_expanded = cost_units_expanded.reindex(df.index)
77
+ df = pd.concat([df.drop(columns=['costUnits']), cost_units_expanded], axis=1)
78
+ else:
79
+ # If costUnits is not dict-like or all None, create columns with None
80
+ for col in ['costUnits.costUnitId', 'costUnits.code', 'costUnits.description']:
81
+ df[col] = None
82
+ df = df.drop(columns=['costUnits'])
83
+
49
84
  return df
50
85
 
51
86
  def update(self, employee_id: str, data: Dict[str, Any]):
@@ -101,7 +136,17 @@ class Costcenter:
101
136
  request = requests.Request(method='GET',
102
137
  url=f"{self.nmbrs.base_url}companies/{company_id}/costcenters")
103
138
  data = self.nmbrs.get_paginated_result(request)
104
- df = pd.DataFrame(data)
139
+
140
+ # Parse and validate API response using Pydantic models
141
+ if not data:
142
+ return pd.DataFrame()
143
+
144
+ # Validate response structure with Pydantic
145
+ response = CostCentersResponse(data=data)
146
+
147
+ # Serialize models to dicts and convert to DataFrame efficiently
148
+ serialized_data = [item.model_dump(by_alias=True, mode='json') for item in response.data]
149
+ df = pd.DataFrame(serialized_data)
105
150
 
106
151
  return df
107
152
 
@@ -1,9 +1,16 @@
1
+ from typing import Any, Dict
2
+
1
3
  import pandas as pd
2
4
  import requests
3
- from typing import Dict, Any
4
5
 
5
6
  from brynq_sdk_functions import Functions
6
- from .schemas.costunit import CostunitGet, CostunitCreate, CostunitUpdate, CostunitDelete
7
+
8
+ from .schemas.costunit import (
9
+ CostunitCreate,
10
+ CostunitDelete,
11
+ CostunitGet,
12
+ CostunitUpdate,
13
+ )
7
14
 
8
15
 
9
16
  class Costunit:
@@ -13,7 +20,9 @@ class Costunit:
13
20
  def get(self) -> tuple[pd.DataFrame, pd.DataFrame]:
14
21
  costunits = pd.DataFrame()
15
22
  for company in self.nmbrs.company_ids:
16
- costunits = pd.concat([costunits, self._get(company)])
23
+ company_costunits = self._get(company)
24
+ if not company_costunits.empty:
25
+ costunits = pd.concat([costunits, company_costunits])
17
26
 
18
27
  valid_costunits, invalid_costunits = Functions.validate_data(df=costunits, schema=CostunitGet, debug=True)
19
28
 
@@ -22,9 +31,12 @@ class Costunit:
22
31
  def _get(self,
23
32
  company_id: str):
24
33
  request = requests.Request(method='GET',
25
- url=f"{self.nmbrs.base_url}companies/{company_id}/costunits")
34
+ url=f"{self.nmbrs.base_url}companies/{company_id}/costUnits")
26
35
 
27
36
  data = self.nmbrs.get_paginated_result(request)
37
+ if not data:
38
+ return pd.DataFrame()
39
+
28
40
  df = pd.DataFrame(data)
29
41
 
30
42
  return df
@@ -1,9 +1,11 @@
1
1
  import pandas as pd
2
2
  import requests
3
+ from typing import Dict, Any
4
+ from zeep.exceptions import Fault
3
5
  from brynq_sdk_functions import Functions
4
6
  from .department import Departments
5
7
  from .function import Functions as NmbrsFunctions
6
- from .schemas.debtor import DebtorsGet
8
+ from .schemas.debtor import DebtorsGet, DebtorCreate, DebtorUpdate
7
9
 
8
10
 
9
11
  class Debtors:
@@ -12,7 +14,6 @@ class Debtors:
12
14
  self.departments = Departments(nmbrs)
13
15
  self.functions = NmbrsFunctions(nmbrs)
14
16
 
15
-
16
17
  def get(self) -> (pd.DataFrame, pd.DataFrame):
17
18
  request = requests.Request(method='GET',
18
19
  url=f"{self.nmbrs.base_url}debtors")
@@ -23,3 +24,76 @@ class Debtors:
23
24
  valid_debtors, invalid_debtors = Functions.validate_data(df=df, schema=DebtorsGet, debug=True)
24
25
 
25
26
  return valid_debtors, invalid_debtors
27
+
28
+ def create(self, data: Dict[str, Any]) -> int:
29
+ """
30
+ Create a new debtor using SOAP API.
31
+
32
+ Args:
33
+ data: Dictionary containing debtor data with fields matching DebtorCreate schema:
34
+ - number: Debtor number
35
+ - name: Debtor name
36
+
37
+ Returns:
38
+ The ID of the newly created debtor.
39
+ """
40
+ debtor_model = DebtorCreate(**data)
41
+
42
+ if self.nmbrs.mock_mode:
43
+ return 12345 # Mock ID
44
+
45
+ try:
46
+ DebtorType = self.nmbrs.soap_client_debtors.get_type('ns0:Debtor')
47
+ soap_debtor = DebtorType(
48
+ Id=0, # 0 for new debtor
49
+ Number=debtor_model.number,
50
+ Name=debtor_model.name
51
+ )
52
+
53
+ response = self.nmbrs.soap_client_debtors.service.Debtor_Insert(
54
+ Debtor=soap_debtor,
55
+ _soapheaders={'AuthHeaderWithDomain': self.nmbrs.soap_auth_header_debtors}
56
+ )
57
+ return response
58
+
59
+ except Fault as e:
60
+ raise Exception(f"SOAP request failed: {str(e)}")
61
+ except Exception as e:
62
+ raise Exception(f"Failed to create Debtor: {str(e)}")
63
+
64
+ def update(self, data: Dict[str, Any]):
65
+ """
66
+ Update a debtor using SOAP API.
67
+
68
+ Args:
69
+ data: Dictionary containing debtor data with fields matching DebtorUpdate schema:
70
+ - debtor_id: Debtor ID to update
71
+ - number: Debtor number
72
+ - name: Debtor name
73
+
74
+ Returns:
75
+ Response from the API
76
+ """
77
+ debtor_model = DebtorUpdate(**data)
78
+
79
+ if self.nmbrs.mock_mode:
80
+ return debtor_model
81
+
82
+ try:
83
+ DebtorType = self.nmbrs.soap_client_debtors.get_type('ns0:Debtor')
84
+ soap_debtor = DebtorType(
85
+ Id=debtor_model.debtor_id,
86
+ Number=debtor_model.number,
87
+ Name=debtor_model.name
88
+ )
89
+
90
+ response = self.nmbrs.soap_client_debtors.service.Debtor_Update(
91
+ Debtor=soap_debtor,
92
+ _soapheaders={'AuthHeaderWithDomain': self.nmbrs.soap_auth_header_debtors}
93
+ )
94
+ return response
95
+
96
+ except Fault as e:
97
+ raise Exception(f"SOAP request failed: {str(e)}")
98
+ except Exception as e:
99
+ raise Exception(f"Failed to update Debtor: {str(e)}")
@@ -1,10 +1,22 @@
1
- import math
1
+ from typing import Any, Dict
2
+
2
3
  import pandas as pd
3
4
  import requests
4
- from typing import Dict, Any
5
- from .schemas.department import DepartmentCreate, EmployeeDepartmentUpdate, Period, EmployeeDepartmentGet, DepartmentGet
5
+ from zeep.exceptions import Fault
6
+
6
7
  from brynq_sdk_functions import Functions
7
8
 
9
+ from .schemas.department import (
10
+ DepartmentCreate,
11
+ DepartmentGet,
12
+ DepartmentMasterCreate,
13
+ DepartmentMasterDelete,
14
+ DepartmentMasterUpdate,
15
+ EmployeeDepartmentGet,
16
+ EmployeeDepartmentUpdate,
17
+ Period,
18
+ )
19
+
8
20
 
9
21
  class EmployeeDepartment:
10
22
  def __init__(self, nmbrs):
@@ -43,35 +55,35 @@ class EmployeeDepartment:
43
55
 
44
56
  return df
45
57
 
46
- def create(self, employee_id: str, data: Dict[str, Any]):
47
- """
48
- Create a new department for an employee using Pydantic validation.
58
+ # def create(self, employee_id: str, data: Dict[str, Any]):
59
+ # """
60
+ # Create a new department for an employee using Pydantic validation.
49
61
 
50
- Args:
51
- employee_id: The ID of the employee
52
- data: Dictionary containing department data with fields matching
53
- the DepartmentCreate schema (using camelCase field names)
62
+ # Args:
63
+ # employee_id: The ID of the employee
64
+ # data: Dictionary containing department data with fields matching
65
+ # the DepartmentCreate schema (using camelCase field names)
54
66
 
55
- Returns:
56
- Response from the API
57
- """
58
- # Validate with Pydantic model
59
- nested_data = self.nmbrs.flat_dict_to_nested_dict(data, DepartmentCreate)
60
- department_model = DepartmentCreate(**nested_data)
67
+ # Returns:
68
+ # Response from the API
69
+ # """
70
+ # # Validate with Pydantic model
71
+ # nested_data = self.nmbrs.flat_dict_to_nested_dict(data, DepartmentCreate)
72
+ # department_model = DepartmentCreate(**nested_data)
61
73
 
62
- if self.nmbrs.mock_mode:
63
- return department_model
74
+ # if self.nmbrs.mock_mode:
75
+ # return department_model
64
76
 
65
- # Convert validated model to dict for API payload
66
- payload = department_model.model_dump(exclude_none=True, by_alias=True)
77
+ # # Convert validated model to dict for API payload
78
+ # payload = department_model.model_dump(exclude_none=True, by_alias=True)
67
79
 
68
- # Send request
69
- resp = self.nmbrs.session.post(
70
- url=f"{self.nmbrs.base_url}employees/{employee_id}/department",
71
- json=payload,
72
- timeout=self.nmbrs.timeout
73
- )
74
- return resp
80
+ # # Send request
81
+ # resp = self.nmbrs.session.post(
82
+ # url=f"{self.nmbrs.base_url}employees/{employee_id}/department",
83
+ # json=payload,
84
+ # timeout=self.nmbrs.timeout
85
+ # )
86
+ # return resp
75
87
 
76
88
  def update(self, employee_id: str, data: Dict[str, Any]):
77
89
  """
@@ -93,7 +105,7 @@ class EmployeeDepartment:
93
105
  return department_model
94
106
 
95
107
  # Convert validated model to dict for API payload
96
- payload = department_model.model_dump(exclude_none=True, by_alias=True)
108
+ payload = department_model.model_dump(exclude_none=True, by_alias=True, mode='json')
97
109
 
98
110
  # Send request
99
111
  resp = self.nmbrs.session.put(
@@ -106,6 +118,8 @@ class EmployeeDepartment:
106
118
 
107
119
 
108
120
  class Departments:
121
+ """Master department operations (Debtor level) - uses SOAP for create/update/delete."""
122
+
109
123
  def __init__(self, nmbrs):
110
124
  self.nmbrs = nmbrs
111
125
 
@@ -120,3 +134,110 @@ class Departments:
120
134
  valid_departments, invalid_departments = Functions.validate_data(df=df, schema=DepartmentGet, debug=True)
121
135
 
122
136
  return valid_departments, invalid_departments
137
+
138
+ def create(self, data: Dict[str, Any]) -> int:
139
+ """
140
+ Create a new master department for a debtor using SOAP API.
141
+
142
+ Args:
143
+ data: Dictionary containing department data with fields matching DepartmentMasterCreate schema:
144
+ - debtor_id: Debtor ID
145
+ - code: Department code
146
+ - description: Department description
147
+
148
+ Returns:
149
+ The ID of the newly created department.
150
+ """
151
+ dept_model = DepartmentMasterCreate(**data)
152
+
153
+ if self.nmbrs.mock_mode:
154
+ return 12345 # Mock ID
155
+
156
+ try:
157
+ DepartmentType = self.nmbrs.soap_client_debtors.get_type('ns0:Department')
158
+ soap_department = DepartmentType(
159
+ Id=0, # 0 for new department
160
+ Code=dept_model.code,
161
+ Description=dept_model.description
162
+ )
163
+
164
+ response = self.nmbrs.soap_client_debtors.service.Department_Insert(
165
+ DebtorId=dept_model.debtor_id,
166
+ department=soap_department,
167
+ _soapheaders={'AuthHeaderWithDomain': self.nmbrs.soap_auth_header_debtors}
168
+ )
169
+ return response
170
+
171
+ except Fault as e:
172
+ raise Exception(f"SOAP request failed: {str(e)}")
173
+ except Exception as e:
174
+ raise Exception(f"Failed to create Department: {str(e)}")
175
+
176
+ def update(self, data: Dict[str, Any]):
177
+ """
178
+ Update a master department for a debtor using SOAP API.
179
+
180
+ Args:
181
+ data: Dictionary containing department data with fields matching DepartmentMasterUpdate schema:
182
+ - debtor_id: Debtor ID
183
+ - department_id: Department ID to update
184
+ - code: Department code
185
+ - description: Department description
186
+
187
+ Returns:
188
+ Response from the API
189
+ """
190
+ dept_model = DepartmentMasterUpdate(**data)
191
+
192
+ if self.nmbrs.mock_mode:
193
+ return dept_model
194
+
195
+ try:
196
+ DepartmentType = self.nmbrs.soap_client_debtors.get_type('ns0:Department')
197
+ soap_department = DepartmentType(
198
+ Id=dept_model.department_id,
199
+ Code=dept_model.code,
200
+ Description=dept_model.description
201
+ )
202
+
203
+ response = self.nmbrs.soap_client_debtors.service.Department_Update(
204
+ DebtorId=dept_model.debtor_id,
205
+ department=soap_department,
206
+ _soapheaders={'AuthHeaderWithDomain': self.nmbrs.soap_auth_header_debtors}
207
+ )
208
+ return response
209
+
210
+ except Fault as e:
211
+ raise Exception(f"SOAP request failed: {str(e)}")
212
+ except Exception as e:
213
+ raise Exception(f"Failed to update Department: {str(e)}")
214
+
215
+ def delete(self, data: Dict[str, Any]):
216
+ """
217
+ Delete a master department for a debtor using SOAP API.
218
+
219
+ Args:
220
+ data: Dictionary containing department data with fields matching DepartmentMasterDelete schema:
221
+ - debtor_id: Debtor ID
222
+ - department_id: Department ID to delete
223
+
224
+ Returns:
225
+ Response from the API
226
+ """
227
+ dept_model = DepartmentMasterDelete(**data)
228
+
229
+ if self.nmbrs.mock_mode:
230
+ return True
231
+
232
+ try:
233
+ response = self.nmbrs.soap_client_debtors.service.Department_Delete(
234
+ DebtorId=dept_model.debtor_id,
235
+ id=dept_model.department_id,
236
+ _soapheaders={'AuthHeaderWithDomain': self.nmbrs.soap_auth_header_debtors}
237
+ )
238
+ return response
239
+
240
+ except Fault as e:
241
+ raise Exception(f"SOAP request failed: {str(e)}")
242
+ except Exception as e:
243
+ raise Exception(f"Failed to delete Department: {str(e)}")
@@ -1,7 +1,57 @@
1
1
  from io import BytesIO
2
+ import base64
3
+ from typing import Dict, Any, Union
2
4
 
3
5
  import pandas as pd
4
6
  import requests
7
+ from zeep.exceptions import Fault
8
+
9
+ from .schemas.document import DocumentUpload
10
+
11
+
12
+ class EmployeeDocument:
13
+ """Handle employee document operations via SOAP API."""
14
+
15
+ def __init__(self, nmbrs):
16
+ self.nmbrs = nmbrs
17
+
18
+ def upload(self, data: Dict[str, Any], file_content: bytes) -> bool:
19
+ """
20
+ Upload a document to an employee using SOAP API.
21
+
22
+ Args:
23
+ data: Dictionary containing document data with fields matching DocumentUpload schema:
24
+ - employee_id: Employee ID
25
+ - document_name: Document name (with extension, e.g., "contract.pdf")
26
+ - document_type_guid: Document type GUID
27
+ file_content: Binary content of the file to upload
28
+
29
+ Returns:
30
+ True if upload was successful
31
+ """
32
+ doc_model = DocumentUpload(**data)
33
+
34
+ if self.nmbrs.mock_mode:
35
+ return True
36
+
37
+ try:
38
+ # Convert file content to base64
39
+ body_base64 = base64.b64encode(file_content)
40
+
41
+ response = self.nmbrs.soap_client_employees.service.EmployeeDocument_UploadDocument(
42
+ EmployeeId=doc_model.employee_id,
43
+ StrDocumentName=doc_model.document_name,
44
+ Body=body_base64,
45
+ GuidDocumentType=doc_model.document_type_guid,
46
+ _soapheaders={'AuthHeaderWithDomain': self.nmbrs.soap_auth_header_employees}
47
+ )
48
+
49
+ return response
50
+
51
+ except Fault as e:
52
+ raise Exception(f"SOAP request failed: {str(e)}")
53
+ except Exception as e:
54
+ raise Exception(f"Failed to upload document: {str(e)}")
5
55
 
6
56
 
7
57
  class Payslip:
@@ -0,0 +1,113 @@
1
+ from typing import Any, Dict
2
+
3
+ import pandas as pd
4
+ import requests
5
+
6
+ from brynq_sdk_functions import Functions
7
+
8
+ from .schemas.wage_tax_settings import (
9
+ EmployeeWageTaxSettingsCreate,
10
+ EmployeeWageTaxSettingsGet,
11
+ )
12
+
13
+
14
+ class EmployeeWageTaxSettings:
15
+ def __init__(self, nmbrs):
16
+ self.nmbrs = nmbrs
17
+
18
+ def get(self,
19
+ employee_id: str = None) -> tuple[pd.DataFrame, pd.DataFrame]:
20
+ """
21
+ Get wage tax settings history for employees in companies.
22
+
23
+ Args:
24
+ employee_id: Optional filter for a specific employee ID
25
+
26
+ Returns:
27
+ Tuple of (valid_data, invalid_data) DataFrames
28
+ """
29
+ wage_tax_settings = pd.DataFrame()
30
+ for company in self.nmbrs.company_ids:
31
+ wage_tax_settings = pd.concat([wage_tax_settings, self._get(company, employee_id)])
32
+
33
+ valid_settings, invalid_settings = Functions.validate_data(
34
+ df=wage_tax_settings, schema=EmployeeWageTaxSettingsGet, debug=True
35
+ )
36
+
37
+ return valid_settings, invalid_settings
38
+
39
+ def _get(self,
40
+ company_id: str,
41
+ employee_id: str = None) -> pd.DataFrame:
42
+ """
43
+ Get wage tax settings history for a specific company.
44
+
45
+ Args:
46
+ company_id: The ID of the company
47
+ employee_id: Optional filter for a specific employee ID
48
+
49
+ Returns:
50
+ DataFrame with wage tax settings
51
+ """
52
+ params = {}
53
+ if employee_id:
54
+ params['employeeId'] = employee_id
55
+
56
+ request = requests.Request(
57
+ method='GET',
58
+ url=f"{self.nmbrs.base_url}companies/{company_id}/employees/wagetaxsettings",
59
+ params=params
60
+ )
61
+
62
+ data = self.nmbrs.get_paginated_result(request)
63
+ df = pd.json_normalize(
64
+ data,
65
+ record_path='wageTaxSettings',
66
+ meta=['employeeId']
67
+ )
68
+
69
+ # Flatten nested Calc30PercentRuling object if it exists
70
+ if 'Calc30PercentRuling' in df.columns:
71
+ calc_ruling_dicts = df['Calc30PercentRuling'].dropna()
72
+ if not calc_ruling_dicts.empty and isinstance(calc_ruling_dicts.iloc[0], dict):
73
+ calc_ruling_expanded = pd.json_normalize(calc_ruling_dicts)
74
+ calc_ruling_expanded.columns = [f'Calc30PercentRuling.{col}' for col in calc_ruling_expanded.columns]
75
+ calc_ruling_expanded = calc_ruling_expanded.reindex(df.index)
76
+ df = pd.concat([df.drop(columns=['Calc30PercentRuling']), calc_ruling_expanded], axis=1)
77
+ else:
78
+ # If Calc30PercentRuling is not dict-like or all None, create columns with None
79
+ for col in ['Calc30PercentRuling.Cal30PercentRuling', 'Calc30PercentRuling.endPeriod', 'Calc30PercentRuling.endYear']:
80
+ df[col] = None
81
+ df = df.drop(columns=['Calc30PercentRuling'])
82
+
83
+ return df
84
+
85
+ def create(self, employee_id: str, data: Dict[str, Any]):
86
+ """
87
+ Create wage tax settings for an employee using Pydantic validation.
88
+
89
+ Args:
90
+ employee_id: The ID of the employee
91
+ data: Dictionary containing wage tax settings data with fields matching
92
+ the EmployeeWageTaxSettingsCreate schema (using camelCase field names)
93
+
94
+ Returns:
95
+ Response from the API
96
+ """
97
+ # Validate with Pydantic model
98
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, EmployeeWageTaxSettingsCreate)
99
+ settings_model = EmployeeWageTaxSettingsCreate(**nested_data)
100
+
101
+ if self.nmbrs.mock_mode:
102
+ return settings_model
103
+
104
+ # Convert validated model to dict for API payload
105
+ payload = settings_model.model_dump(exclude_none=True, by_alias=True, mode='json')
106
+
107
+ # Send request
108
+ resp = self.nmbrs.session.post(
109
+ url=f"{self.nmbrs.base_url}employees/{employee_id}/wagetaxsettings",
110
+ json=payload,
111
+ timeout=self.nmbrs.timeout
112
+ )
113
+ return resp