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.
- brynq_sdk_nmbrs/__init__.py +104 -84
- brynq_sdk_nmbrs/address.py +82 -2
- brynq_sdk_nmbrs/children.py +96 -61
- brynq_sdk_nmbrs/companies.py +45 -1
- brynq_sdk_nmbrs/costcenter.py +53 -8
- brynq_sdk_nmbrs/costunit.py +16 -4
- brynq_sdk_nmbrs/debtors.py +76 -2
- brynq_sdk_nmbrs/department.py +149 -28
- brynq_sdk_nmbrs/document.py +50 -0
- brynq_sdk_nmbrs/employee_wage_tax_settings.py +113 -0
- brynq_sdk_nmbrs/employees.py +119 -24
- brynq_sdk_nmbrs/employment.py +12 -4
- brynq_sdk_nmbrs/function.py +128 -2
- brynq_sdk_nmbrs/leave.py +105 -8
- brynq_sdk_nmbrs/salaries.py +78 -3
- brynq_sdk_nmbrs/schedules.py +77 -3
- brynq_sdk_nmbrs/schemas/address.py +30 -5
- brynq_sdk_nmbrs/schemas/bank.py +0 -2
- brynq_sdk_nmbrs/schemas/children.py +67 -0
- brynq_sdk_nmbrs/schemas/company.py +16 -0
- brynq_sdk_nmbrs/schemas/contracts.py +25 -11
- brynq_sdk_nmbrs/schemas/costcenter.py +57 -18
- brynq_sdk_nmbrs/schemas/costunit.py +0 -2
- brynq_sdk_nmbrs/schemas/days.py +0 -2
- brynq_sdk_nmbrs/schemas/debtor.py +23 -1
- brynq_sdk_nmbrs/schemas/department.py +41 -7
- brynq_sdk_nmbrs/schemas/document.py +13 -0
- brynq_sdk_nmbrs/schemas/employees.py +44 -38
- brynq_sdk_nmbrs/schemas/employment.py +10 -10
- brynq_sdk_nmbrs/schemas/function.py +34 -7
- brynq_sdk_nmbrs/schemas/hours.py +0 -4
- brynq_sdk_nmbrs/schemas/leave.py +12 -1
- brynq_sdk_nmbrs/schemas/manager.py +0 -3
- brynq_sdk_nmbrs/schemas/salary.py +37 -12
- brynq_sdk_nmbrs/schemas/schedules.py +49 -1
- brynq_sdk_nmbrs/schemas/social_insurance.py +39 -6
- brynq_sdk_nmbrs/schemas/wage_tax.py +68 -8
- brynq_sdk_nmbrs/schemas/wage_tax_settings.py +76 -0
- brynq_sdk_nmbrs/schemas/wagecomponents.py +0 -4
- brynq_sdk_nmbrs/social_insurance.py +81 -3
- brynq_sdk_nmbrs/wage_tax.py +105 -4
- {brynq_sdk_nmbrs-2.3.1.dist-info → brynq_sdk_nmbrs-2.3.2.dev0.dist-info}/METADATA +1 -1
- brynq_sdk_nmbrs-2.3.2.dev0.dist-info/RECORD +55 -0
- {brynq_sdk_nmbrs-2.3.1.dist-info → brynq_sdk_nmbrs-2.3.2.dev0.dist-info}/WHEEL +1 -1
- brynq_sdk_nmbrs-2.3.1.dist-info/RECORD +0 -50
- {brynq_sdk_nmbrs-2.3.1.dist-info → brynq_sdk_nmbrs-2.3.2.dev0.dist-info}/top_level.txt +0 -0
brynq_sdk_nmbrs/costcenter.py
CHANGED
|
@@ -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
|
-
|
|
9
|
-
from .schemas.costcenter import
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
brynq_sdk_nmbrs/costunit.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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}/
|
|
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
|
brynq_sdk_nmbrs/debtors.py
CHANGED
|
@@ -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)}")
|
brynq_sdk_nmbrs/department.py
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
2
3
|
import pandas as pd
|
|
3
4
|
import requests
|
|
4
|
-
from
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
74
|
+
# if self.nmbrs.mock_mode:
|
|
75
|
+
# return department_model
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
# # Convert validated model to dict for API payload
|
|
78
|
+
# payload = department_model.model_dump(exclude_none=True, by_alias=True)
|
|
67
79
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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)}")
|
brynq_sdk_nmbrs/document.py
CHANGED
|
@@ -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
|