brynq-sdk-nmbrs 2.3.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.
- brynq_sdk_nmbrs/__init__.py +226 -0
- brynq_sdk_nmbrs/absence.py +124 -0
- brynq_sdk_nmbrs/address.py +66 -0
- brynq_sdk_nmbrs/bank.py +125 -0
- brynq_sdk_nmbrs/children.py +100 -0
- brynq_sdk_nmbrs/companies.py +93 -0
- brynq_sdk_nmbrs/contract.py +132 -0
- brynq_sdk_nmbrs/costcenter.py +166 -0
- brynq_sdk_nmbrs/costunit.py +90 -0
- brynq_sdk_nmbrs/days.py +137 -0
- brynq_sdk_nmbrs/debtors.py +25 -0
- brynq_sdk_nmbrs/department.py +122 -0
- brynq_sdk_nmbrs/document.py +30 -0
- brynq_sdk_nmbrs/employees.py +196 -0
- brynq_sdk_nmbrs/employment.py +107 -0
- brynq_sdk_nmbrs/function.py +89 -0
- brynq_sdk_nmbrs/hours.py +252 -0
- brynq_sdk_nmbrs/leave.py +139 -0
- brynq_sdk_nmbrs/manager.py +294 -0
- brynq_sdk_nmbrs/salaries.py +85 -0
- brynq_sdk_nmbrs/salary_tables.py +242 -0
- brynq_sdk_nmbrs/schedules.py +84 -0
- brynq_sdk_nmbrs/schemas/__init__.py +37 -0
- brynq_sdk_nmbrs/schemas/absence.py +61 -0
- brynq_sdk_nmbrs/schemas/address.py +76 -0
- brynq_sdk_nmbrs/schemas/bank.py +83 -0
- brynq_sdk_nmbrs/schemas/contracts.py +60 -0
- brynq_sdk_nmbrs/schemas/costcenter.py +91 -0
- brynq_sdk_nmbrs/schemas/costunit.py +40 -0
- brynq_sdk_nmbrs/schemas/days.py +98 -0
- brynq_sdk_nmbrs/schemas/debtor.py +16 -0
- brynq_sdk_nmbrs/schemas/department.py +57 -0
- brynq_sdk_nmbrs/schemas/employees.py +153 -0
- brynq_sdk_nmbrs/schemas/employment.py +48 -0
- brynq_sdk_nmbrs/schemas/function.py +50 -0
- brynq_sdk_nmbrs/schemas/hours.py +121 -0
- brynq_sdk_nmbrs/schemas/leave.py +53 -0
- brynq_sdk_nmbrs/schemas/manager.py +126 -0
- brynq_sdk_nmbrs/schemas/salary.py +92 -0
- brynq_sdk_nmbrs/schemas/schedules.py +96 -0
- brynq_sdk_nmbrs/schemas/social_insurance.py +40 -0
- brynq_sdk_nmbrs/schemas/wage_tax.py +98 -0
- brynq_sdk_nmbrs/schemas/wagecomponents.py +114 -0
- brynq_sdk_nmbrs/social_insurance.py +52 -0
- brynq_sdk_nmbrs/wage_tax.py +164 -0
- brynq_sdk_nmbrs/wagecomponents.py +268 -0
- brynq_sdk_nmbrs-2.3.1.dist-info/METADATA +21 -0
- brynq_sdk_nmbrs-2.3.1.dist-info/RECORD +50 -0
- brynq_sdk_nmbrs-2.3.1.dist-info/WHEEL +5 -0
- brynq_sdk_nmbrs-2.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import Union, List, Literal, Optional, get_args, get_origin
|
|
3
|
+
import re
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
import requests
|
|
7
|
+
from brynq_sdk_brynq import BrynQ
|
|
8
|
+
from .absence import Absence
|
|
9
|
+
from .wage_tax import WageTax
|
|
10
|
+
from .address import Address
|
|
11
|
+
from .bank import Bank
|
|
12
|
+
from zeep import Client, Settings
|
|
13
|
+
from .children import Children
|
|
14
|
+
from .companies import Companies
|
|
15
|
+
from .debtors import Debtors
|
|
16
|
+
from .contract import Contract
|
|
17
|
+
from .costcenter import EmployeeCostcenter, Costcenter
|
|
18
|
+
from .costunit import Costunit
|
|
19
|
+
from .department import EmployeeDepartment
|
|
20
|
+
from .document import Payslip
|
|
21
|
+
from .employees import Employees
|
|
22
|
+
from .salary_tables import SalaryTables, SalaryScales, SalarySteps
|
|
23
|
+
from .employment import Employment
|
|
24
|
+
from .function import EmployeeFunction
|
|
25
|
+
from .hours import VariableHours, FixedHours
|
|
26
|
+
from .days import VariableDays, FixedDays
|
|
27
|
+
from .manager import EmployeeManager, Manager
|
|
28
|
+
from .salaries import Salaries
|
|
29
|
+
from .schedules import Schedule
|
|
30
|
+
from .wagecomponents import EmployeeFixedWageComponents, EmployeeVariableWageComponents
|
|
31
|
+
import os
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Nmbrs(BrynQ):
|
|
35
|
+
def __init__(self, system_type: Optional[Literal['source', 'target']] = None, debug: bool = False, mock_mode: bool = True):
|
|
36
|
+
"""
|
|
37
|
+
Initialize the Nmbrs class.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
label: The label of the system in BrynQ. legacy
|
|
41
|
+
debug: Whether to print debug information
|
|
42
|
+
11 mock_mode: If true, data will NOT be sent to Nmbrs but only be tested for validity against Pydantic schemas
|
|
43
|
+
"""
|
|
44
|
+
self.mock_mode = mock_mode
|
|
45
|
+
self.debug = debug
|
|
46
|
+
self.timeout = 3600
|
|
47
|
+
self.system_type = system_type
|
|
48
|
+
if mock_mode is False:
|
|
49
|
+
super().__init__()
|
|
50
|
+
self.data_interface_id = os.getenv("DATA_INTERFACE_ID")
|
|
51
|
+
headers = self._get_request_headers()
|
|
52
|
+
self.base_url = "https://api.nmbrsapp.com/api/"
|
|
53
|
+
self.session = requests.Session()
|
|
54
|
+
self.session.headers.update(headers)
|
|
55
|
+
|
|
56
|
+
# Initialize SOAP client
|
|
57
|
+
self.soap_settings = Settings(
|
|
58
|
+
strict=False,
|
|
59
|
+
xml_huge_tree=True,
|
|
60
|
+
force_https=True
|
|
61
|
+
)
|
|
62
|
+
self.soap_client_companies = Client(
|
|
63
|
+
'https://api.nmbrs.nl/soap/v3/CompanyService.asmx?wsdl',
|
|
64
|
+
settings=self.soap_settings
|
|
65
|
+
)
|
|
66
|
+
self.soap_client_employees = Client(
|
|
67
|
+
'https://api.nmbrs.nl/soap/v3/EmployeeService.asmx?wsdl',
|
|
68
|
+
settings=self.soap_settings
|
|
69
|
+
)
|
|
70
|
+
self.soap_auth_header = self._get_soap_auth_header()
|
|
71
|
+
# Following methods can only be used if the SOAP authentication header is set (optional, this is not always in scope for all integrations)
|
|
72
|
+
if self.soap_auth_header is not None:
|
|
73
|
+
self.soap_company_ids = self.companies.get_soap_ids()
|
|
74
|
+
self.salary_tables = SalaryTables(self)
|
|
75
|
+
self.salary_scales = SalaryScales(self)
|
|
76
|
+
self.salary_steps = SalarySteps(self)
|
|
77
|
+
self.wage_tax = WageTax(self)
|
|
78
|
+
self.absence = Absence(self)
|
|
79
|
+
self.children = Children(self)
|
|
80
|
+
|
|
81
|
+
self.address = Address(self)
|
|
82
|
+
self.bank = Bank(self)
|
|
83
|
+
self.children = Children(self)
|
|
84
|
+
self.debtor = Debtors(self)
|
|
85
|
+
self.companies = Companies(self)
|
|
86
|
+
self.contract = Contract(self)
|
|
87
|
+
self.department = EmployeeDepartment(self)
|
|
88
|
+
debtors, _ = self.debtor.get()
|
|
89
|
+
self.debtor_ids = debtors['debtor_id'].to_list()
|
|
90
|
+
self.company_ids = self.companies.get()['companyId'].to_list()
|
|
91
|
+
self.employees = Employees(self)
|
|
92
|
+
self.employment = Employment(self)
|
|
93
|
+
self.function = EmployeeFunction(self)
|
|
94
|
+
self.fixed_hours = FixedHours(self)
|
|
95
|
+
self.fixed_days = FixedDays(self)
|
|
96
|
+
self.variable_hours = VariableHours(self)
|
|
97
|
+
self.variable_days = VariableDays(self)
|
|
98
|
+
self.manager = Manager(self)
|
|
99
|
+
self.employee_manager = EmployeeManager(self)
|
|
100
|
+
self.salaries = Salaries(self)
|
|
101
|
+
self.schedule = Schedule(self)
|
|
102
|
+
self.fixed_wagecomponents = EmployeeFixedWageComponents(self)
|
|
103
|
+
self.variable_wagecomponents = EmployeeVariableWageComponents(self)
|
|
104
|
+
self.current_period = self.companies.get_current_period()
|
|
105
|
+
|
|
106
|
+
def _get_request_headers(self):
|
|
107
|
+
credentials = self.interfaces.credentials.get(system='nmbrs', system_type=self.system_type)
|
|
108
|
+
headers = {
|
|
109
|
+
"accept": "application/json",
|
|
110
|
+
"Authorization": f"Bearer {credentials.get('data').get('access_token')}",
|
|
111
|
+
# partner identifier
|
|
112
|
+
"X-Subscription-Key": credentials.get("custom_data").get("subscription_key")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return headers
|
|
116
|
+
|
|
117
|
+
def _get_soap_auth_header(self):
|
|
118
|
+
"""
|
|
119
|
+
Creates the SOAP authentication header using credentials from initial_credentials.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
AuthHeaderWithDomainType: The authentication header for SOAP requests
|
|
123
|
+
"""
|
|
124
|
+
initial_credentials = self.interfaces.credentials.get(system='nmbrs', system_type=self.system_type)
|
|
125
|
+
config = initial_credentials.get("custom_data", {})
|
|
126
|
+
|
|
127
|
+
if 'soap_api_token' not in config.keys():
|
|
128
|
+
return None
|
|
129
|
+
else:
|
|
130
|
+
# Get the AuthHeaderWithDomain type from the WSDL
|
|
131
|
+
AuthHeaderWithDomainType = self.soap_client_companies.get_element('ns0:AuthHeaderWithDomain')
|
|
132
|
+
|
|
133
|
+
# Create the auth header using credentials from config
|
|
134
|
+
auth_header = AuthHeaderWithDomainType(
|
|
135
|
+
Username=config.get("soap_api_username"),
|
|
136
|
+
Token=config.get("soap_api_token"),
|
|
137
|
+
Domain=config.get("soap_api_domain")
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return auth_header
|
|
141
|
+
|
|
142
|
+
def _get_soap_auth_header_employees(self):
|
|
143
|
+
"""
|
|
144
|
+
Creates the SOAP authentication header using credentials from initial_credentials.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
AuthHeaderWithDomainType: The authentication header for SOAP requests
|
|
148
|
+
"""
|
|
149
|
+
initial_credentials = self.get_system_credential(system='nmbrs', label='bob')
|
|
150
|
+
config = initial_credentials.get("config", {})
|
|
151
|
+
|
|
152
|
+
# Get the AuthHeaderWithDomain type from the WSDL
|
|
153
|
+
AuthHeaderWithDomainType = self.soap_client_employees.get_element('ns0:AuthHeaderWithDomain')
|
|
154
|
+
|
|
155
|
+
# Create the auth header using credentials from config
|
|
156
|
+
auth_header = AuthHeaderWithDomainType(
|
|
157
|
+
Username=config.get("soap_api_username"),
|
|
158
|
+
Token=config.get("soap_api_token"),
|
|
159
|
+
Domain=config.get("soap_api_domain")
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return auth_header
|
|
163
|
+
|
|
164
|
+
def get_paginated_result(self, request: requests.Request) -> List:
|
|
165
|
+
has_next_page = True
|
|
166
|
+
result_data = []
|
|
167
|
+
while has_next_page:
|
|
168
|
+
prepped = request.prepare()
|
|
169
|
+
prepped.headers.update(self.session.headers)
|
|
170
|
+
resp = self.session.send(prepped, timeout=self.timeout)
|
|
171
|
+
resp.raise_for_status()
|
|
172
|
+
response_data = resp.json()
|
|
173
|
+
result_data += response_data['data']
|
|
174
|
+
next_page_url = response_data.get('pagination').get('nextPage')
|
|
175
|
+
has_next_page = next_page_url is not None
|
|
176
|
+
request.url = next_page_url
|
|
177
|
+
|
|
178
|
+
return result_data
|
|
179
|
+
|
|
180
|
+
def check_fields(self, data: Union[dict, List], required_fields: List, allowed_fields: List):
|
|
181
|
+
if isinstance(data, dict):
|
|
182
|
+
data = data.keys()
|
|
183
|
+
|
|
184
|
+
if self.debug:
|
|
185
|
+
print(f"Required fields: {required_fields}")
|
|
186
|
+
print(f"Allowed fields: {allowed_fields}")
|
|
187
|
+
print(f"Data: {data}")
|
|
188
|
+
|
|
189
|
+
for field in data:
|
|
190
|
+
if field not in allowed_fields and field not in required_fields:
|
|
191
|
+
warnings.warn('Field {field} is not implemented. Optional fields are: {allowed_fields}'.format(field=field, allowed_fields=tuple(allowed_fields)))
|
|
192
|
+
|
|
193
|
+
for field in required_fields:
|
|
194
|
+
if field not in data:
|
|
195
|
+
raise ValueError('Field {field} is required. Required fields are: {required_fields}'.format(field=field, required_fields=tuple(required_fields)))
|
|
196
|
+
|
|
197
|
+
def _rename_camel_columns_to_snake_case(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
198
|
+
def camel_to_snake_case(column):
|
|
199
|
+
# Replace periods with underscores
|
|
200
|
+
column = column.replace('.', '_')
|
|
201
|
+
# Insert underscores before capital letters and convert to lowercase
|
|
202
|
+
return re.sub(r'(?<!^)(?=[A-Z])', '_', column).lower()
|
|
203
|
+
|
|
204
|
+
df.columns = map(camel_to_snake_case, df.columns)
|
|
205
|
+
|
|
206
|
+
return df
|
|
207
|
+
|
|
208
|
+
def flat_dict_to_nested_dict(self, flat_dict: dict, model: BaseModel) -> dict:
|
|
209
|
+
nested = {}
|
|
210
|
+
for name, field in model.model_fields.items():
|
|
211
|
+
key_in_input = name # Original model field name as key in flat_dict
|
|
212
|
+
alias = field.alias or name
|
|
213
|
+
if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
|
|
214
|
+
nested[alias] = self.flat_dict_to_nested_dict(flat_dict, field.annotation)
|
|
215
|
+
elif any(isinstance(item, type) and issubclass(item, BaseModel) for item in get_args(field.annotation)):
|
|
216
|
+
# get the basemodel class from the list
|
|
217
|
+
nested_model_name = [item for item in get_args(field.annotation) if isinstance(item, type) and issubclass(item, BaseModel)][0]
|
|
218
|
+
origin = get_origin(field.annotation)
|
|
219
|
+
if origin in (list, List):
|
|
220
|
+
nested[alias] = [self.flat_dict_to_nested_dict(flat_dict, nested_model_name)]
|
|
221
|
+
else:
|
|
222
|
+
nested[alias] = self.flat_dict_to_nested_dict(flat_dict, nested_model_name)
|
|
223
|
+
else:
|
|
224
|
+
if key_in_input in flat_dict:
|
|
225
|
+
nested[alias] = flat_dict[key_in_input]
|
|
226
|
+
return nested
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Union, Tuple
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from .schemas.absence import AbsenceCreate, AbsenceGet
|
|
4
|
+
from zeep.exceptions import Fault
|
|
5
|
+
from zeep.ns import WSDL, SOAP_ENV_11
|
|
6
|
+
from zeep.xsd import ComplexType, Element, String
|
|
7
|
+
from zeep.helpers import serialize_object
|
|
8
|
+
# import logging
|
|
9
|
+
from brynq_sdk_functions import Functions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Absence:
|
|
13
|
+
def __init__(self, nmbrs):
|
|
14
|
+
self.nmbrs = nmbrs
|
|
15
|
+
self.soap_client_companies = nmbrs.soap_client_companies
|
|
16
|
+
self.soap_client_employees = nmbrs.soap_client_employees
|
|
17
|
+
|
|
18
|
+
def get(self, employee_id: int = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
19
|
+
"""
|
|
20
|
+
Get salary tables for all companies for a specific period and year.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
period (int): The period number
|
|
24
|
+
year (int): The year
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
pd.DataFrame: DataFrame containing the salary tables
|
|
28
|
+
"""
|
|
29
|
+
absences = pd.DataFrame()
|
|
30
|
+
for company in self.nmbrs.soap_company_ids.to_dict(orient='records'):
|
|
31
|
+
absences_temp = self._get(company['i_d'], employee_id=employee_id)
|
|
32
|
+
if not absences_temp.empty:
|
|
33
|
+
absences_temp['companyId'] = company['number']
|
|
34
|
+
absences = pd.concat([absences, absences_temp])
|
|
35
|
+
|
|
36
|
+
valid_absences, invalid_absences = Functions.validate_data(df=absences, schema=AbsenceGet, debug=True)
|
|
37
|
+
|
|
38
|
+
# No validation schema for now, but could be added later
|
|
39
|
+
return valid_absences, invalid_absences
|
|
40
|
+
|
|
41
|
+
def _get(self, company_id: int, employee_id: int = None) -> pd.DataFrame:
|
|
42
|
+
"""
|
|
43
|
+
Get all absences for a specific company, period and year.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
company_id (int): The ID of the company
|
|
47
|
+
period (int): The period number
|
|
48
|
+
year (int): The year
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
pd.DataFrame: DataFrame containing the salary tables
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
if employee_id is None:
|
|
55
|
+
response = self.soap_client_employees.service.Absence_GetAll_AllEmployeesByCompany(
|
|
56
|
+
CompanyId=company_id,
|
|
57
|
+
_soapheaders=[self.nmbrs.soap_auth_header]
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
# Make SOAP request with the proper header structure
|
|
61
|
+
response = self.soap_client_employees.service.Absence_GetList(
|
|
62
|
+
EmployeeId=employee_id,
|
|
63
|
+
_soapheaders=[self.nmbrs.soap_auth_header]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Convert response to DataFrame
|
|
67
|
+
if response:
|
|
68
|
+
# Convert Zeep objects to Python dictionaries
|
|
69
|
+
serialized_response = serialize_object(response)
|
|
70
|
+
|
|
71
|
+
# Convert to list if it's not already
|
|
72
|
+
if not isinstance(serialized_response, list):
|
|
73
|
+
serialized_response = [serialized_response]
|
|
74
|
+
|
|
75
|
+
# Convert to DataFrame
|
|
76
|
+
df = pd.DataFrame(serialized_response)
|
|
77
|
+
|
|
78
|
+
return df
|
|
79
|
+
else:
|
|
80
|
+
return pd.DataFrame()
|
|
81
|
+
|
|
82
|
+
except Fault as e:
|
|
83
|
+
raise Exception(f"SOAP request failed: {str(e)}")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise Exception(f"Failed to get salary tables: {str(e)}")
|
|
86
|
+
|
|
87
|
+
def create(self, data: Dict[str, Any]) -> pd.DataFrame:
|
|
88
|
+
try:
|
|
89
|
+
absence_model = AbsenceCreate(**data)
|
|
90
|
+
|
|
91
|
+
if self.nmbrs.mock_mode:
|
|
92
|
+
return absence_model
|
|
93
|
+
|
|
94
|
+
# Use the model's built-in SOAP conversion method
|
|
95
|
+
absence_settings = absence_model.to_soap_settings(self.nmbrs.soap_client_employees)
|
|
96
|
+
|
|
97
|
+
# Make SOAP request with clean, simple call
|
|
98
|
+
response = self.nmbrs.soap_client_employees.service.Absence_Create(
|
|
99
|
+
EmployeeId=absence_model.employee_id,
|
|
100
|
+
Dossier=absence_model.new_dossier,
|
|
101
|
+
Absence=absence_settings,
|
|
102
|
+
_soapheaders=[self.nmbrs.soap_auth_header]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Convert response to DataFrame
|
|
106
|
+
if response:
|
|
107
|
+
# Convert Zeep objects to Python dictionaries
|
|
108
|
+
serialized_response = serialize_object(response)
|
|
109
|
+
|
|
110
|
+
# Convert to list if it's not already
|
|
111
|
+
if not isinstance(serialized_response, list):
|
|
112
|
+
serialized_response = [serialized_response]
|
|
113
|
+
|
|
114
|
+
# Convert to DataFrame
|
|
115
|
+
df = pd.DataFrame(serialized_response)
|
|
116
|
+
|
|
117
|
+
return df
|
|
118
|
+
else:
|
|
119
|
+
return pd.DataFrame()
|
|
120
|
+
|
|
121
|
+
except Fault as e:
|
|
122
|
+
raise Exception(f"SOAP request failed: {str(e)}")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise Exception(f"Failed to update WageTax: {str(e)}")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import requests
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from .schemas.address import AddressCreate, AddressGet, Period
|
|
5
|
+
from brynq_sdk_functions import Functions
|
|
6
|
+
|
|
7
|
+
class Address:
|
|
8
|
+
def __init__(self, nmbrs):
|
|
9
|
+
self.nmbrs = nmbrs
|
|
10
|
+
|
|
11
|
+
def get(self,
|
|
12
|
+
created_from: str = None) -> pd.DataFrame:
|
|
13
|
+
addresses = pd.DataFrame()
|
|
14
|
+
for company in self.nmbrs.company_ids:
|
|
15
|
+
addresses = pd.concat([addresses, self._get(company, created_from)])
|
|
16
|
+
|
|
17
|
+
valid_addresses, invalid_addresses = Functions.validate_data(df=addresses, schema=AddressGet, debug=True)
|
|
18
|
+
|
|
19
|
+
return valid_addresses, invalid_addresses
|
|
20
|
+
|
|
21
|
+
def _get(self,
|
|
22
|
+
company_id: str,
|
|
23
|
+
created_from: str = None) -> pd.DataFrame:
|
|
24
|
+
params = {} if created_from is None else {'createdFrom': created_from}
|
|
25
|
+
request = requests.Request(method='GET',
|
|
26
|
+
url=f"{self.nmbrs.base_url}companies/{company_id}/employees/addresses",
|
|
27
|
+
params=params)
|
|
28
|
+
|
|
29
|
+
data = self.nmbrs.get_paginated_result(request)
|
|
30
|
+
df = pd.json_normalize(
|
|
31
|
+
data,
|
|
32
|
+
record_path='addresses',
|
|
33
|
+
meta=['employeeId']
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return df
|
|
37
|
+
|
|
38
|
+
def create(self, employee_id: str, data: Dict[str, Any]):
|
|
39
|
+
"""
|
|
40
|
+
Create a new address for an employee using Pydantic validation.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
employee_id: The ID of the employee
|
|
44
|
+
data: Dictionary containing address data with fields matching
|
|
45
|
+
the AddressCreate schema (using camelCase field names)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Response from the API
|
|
49
|
+
"""
|
|
50
|
+
# Validate with Pydantic model - this will raise an error if required fields are missing
|
|
51
|
+
nested_data = self.nmbrs.flat_dict_to_nested_dict(data, AddressCreate)
|
|
52
|
+
address_model = AddressCreate(**nested_data)
|
|
53
|
+
|
|
54
|
+
if self.nmbrs.mock_mode:
|
|
55
|
+
return address_model
|
|
56
|
+
|
|
57
|
+
# Convert validated model to dict for API payload
|
|
58
|
+
payload = address_model.model_dump(exclude_none=True, by_alias=True)
|
|
59
|
+
|
|
60
|
+
# Send request
|
|
61
|
+
resp = self.nmbrs.session.post(
|
|
62
|
+
url=f"{self.nmbrs.base_url}employees/{employee_id}/address",
|
|
63
|
+
json=payload,
|
|
64
|
+
timeout=self.nmbrs.timeout
|
|
65
|
+
)
|
|
66
|
+
return resp
|
brynq_sdk_nmbrs/bank.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import requests
|
|
4
|
+
from brynq_sdk_functions import Functions
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
from .schemas.bank import BankGet, BankCreate, BankUpdate, BankDelete
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Bank:
|
|
10
|
+
def __init__(self, nmbrs):
|
|
11
|
+
self.nmbrs = nmbrs
|
|
12
|
+
|
|
13
|
+
def get(self,
|
|
14
|
+
created_from: str = None) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
15
|
+
banks = pd.DataFrame()
|
|
16
|
+
for company in self.nmbrs.company_ids:
|
|
17
|
+
banks = pd.concat([banks, self._get(company, created_from)])
|
|
18
|
+
|
|
19
|
+
valid_banks, invalid_banks = Functions.validate_data(df=banks, schema=BankGet, debug=True)
|
|
20
|
+
|
|
21
|
+
return valid_banks, invalid_banks
|
|
22
|
+
|
|
23
|
+
def _get(self,
|
|
24
|
+
company_id: str,
|
|
25
|
+
created_from: str = None) -> pd.DataFrame:
|
|
26
|
+
params = {}
|
|
27
|
+
if created_from:
|
|
28
|
+
params['createdFrom'] = created_from
|
|
29
|
+
try:
|
|
30
|
+
request = requests.Request(method='GET',
|
|
31
|
+
url=f"{self.nmbrs.base_url}companies/{company_id}/employees/bankaccounts",
|
|
32
|
+
params=params)
|
|
33
|
+
|
|
34
|
+
data = self.nmbrs.get_paginated_result(request)
|
|
35
|
+
df = pd.json_normalize(
|
|
36
|
+
data,
|
|
37
|
+
record_path='bankAccounts',
|
|
38
|
+
meta=['employeeId']
|
|
39
|
+
)
|
|
40
|
+
except requests.HTTPError as e:
|
|
41
|
+
df = pd.DataFrame()
|
|
42
|
+
|
|
43
|
+
return df
|
|
44
|
+
|
|
45
|
+
def create(self, employee_id: str, data: Dict[str, Any]):
|
|
46
|
+
"""
|
|
47
|
+
Create a new bank account for an employee using Pydantic validation.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
employee_id: The ID of the employee
|
|
51
|
+
data: Dictionary containing bank account data in the format matching BankCreate schema
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Response from the API
|
|
55
|
+
"""
|
|
56
|
+
# Validate with Pydantic model - this will raise an error if required fields are missing
|
|
57
|
+
nested_data = self.nmbrs.flat_dict_to_nested_dict(data, BankCreate)
|
|
58
|
+
bank_model = BankCreate(**nested_data)
|
|
59
|
+
|
|
60
|
+
if self.nmbrs.mock_mode:
|
|
61
|
+
return bank_model
|
|
62
|
+
|
|
63
|
+
# Convert validated model to dict for API payload
|
|
64
|
+
payload = bank_model.model_dump(exclude_none=True, by_alias=True)
|
|
65
|
+
|
|
66
|
+
# Send request
|
|
67
|
+
resp = self.nmbrs.session.post(
|
|
68
|
+
url=f"{self.nmbrs.base_url}employees/{employee_id}/bankaccount",
|
|
69
|
+
json=payload,
|
|
70
|
+
timeout=self.nmbrs.timeout
|
|
71
|
+
)
|
|
72
|
+
return resp
|
|
73
|
+
|
|
74
|
+
def update(self, employee_id: str, data: Dict[str, Any]):
|
|
75
|
+
"""
|
|
76
|
+
Update a bank account for an employee using Pydantic validation.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
employee_id: The ID of the employee
|
|
80
|
+
data: Dictionary containing bank account data in the format matching BankUpdate schema
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Response from the API
|
|
84
|
+
"""
|
|
85
|
+
# Validate with Pydantic model - this will raise an error if required fields are missing
|
|
86
|
+
nested_data = self.nmbrs.flat_dict_to_nested_dict(data, BankUpdate)
|
|
87
|
+
bank_model = BankUpdate(**nested_data)
|
|
88
|
+
|
|
89
|
+
if self.nmbrs.mock_mode:
|
|
90
|
+
return bank_model
|
|
91
|
+
|
|
92
|
+
# Convert validated model to dict for API payload
|
|
93
|
+
payload = bank_model.model_dump(exclude_none=True, by_alias=True)
|
|
94
|
+
|
|
95
|
+
# Send request
|
|
96
|
+
resp = self.nmbrs.session.put(
|
|
97
|
+
url=f"{self.nmbrs.base_url}employees/{employee_id}/bankaccount",
|
|
98
|
+
json=payload,
|
|
99
|
+
timeout=self.nmbrs.timeout
|
|
100
|
+
)
|
|
101
|
+
return resp
|
|
102
|
+
|
|
103
|
+
def delete(self, employee_id: str, bank_account_id: str):
|
|
104
|
+
"""
|
|
105
|
+
Delete a bank account for an employee.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
employee_id: The ID of the employee
|
|
109
|
+
bank_account_id: The ID of the bank account to delete
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Response from the API
|
|
113
|
+
"""
|
|
114
|
+
# Create and validate a BankDelete model
|
|
115
|
+
bank_model = BankDelete(bankAccountId=bank_account_id)
|
|
116
|
+
|
|
117
|
+
if self.nmbrs.mock_mode:
|
|
118
|
+
return bank_model
|
|
119
|
+
|
|
120
|
+
# Send request
|
|
121
|
+
resp = self.nmbrs.session.delete(
|
|
122
|
+
url=f"{self.nmbrs.base_url}employees/{employee_id}/bankaccounts/{bank_account_id}",
|
|
123
|
+
timeout=self.nmbrs.timeout
|
|
124
|
+
)
|
|
125
|
+
return resp
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from zeep import Client
|
|
4
|
+
from zeep.transports import Transport
|
|
5
|
+
import requests
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Children:
|
|
10
|
+
def __init__(self, nmbrs):
|
|
11
|
+
self.nmbrs = nmbrs
|
|
12
|
+
self.client = Client(wsdl='https://api.nmbrs.nl/soap/v3/EmployeeService.asmx?wsdl') #, transport=Transport(session=self.nmbrs.session))
|
|
13
|
+
# self.client.set_default_soapheaders([auth_header])
|
|
14
|
+
AuthHeaderWithDomainType = self.client.get_element('ns0:AuthHeaderWithDomain')
|
|
15
|
+
|
|
16
|
+
auth_header = AuthHeaderWithDomainType(
|
|
17
|
+
Username="erwin.vink@brynq.com",
|
|
18
|
+
Token="cc358715f5c14cda8add964deef99ba3",
|
|
19
|
+
Domain="extdev-brynq"
|
|
20
|
+
)
|
|
21
|
+
self.client.set_default_soapheaders([auth_header])
|
|
22
|
+
|
|
23
|
+
def get(self,
|
|
24
|
+
company_id: str,
|
|
25
|
+
created_from: str = None) -> pd.DataFrame:
|
|
26
|
+
params = {}
|
|
27
|
+
if created_from:
|
|
28
|
+
params['createdFrom'] = created_from
|
|
29
|
+
try:
|
|
30
|
+
request = requests.Request(method='GET',
|
|
31
|
+
url=f"{self.nmbrs.base_url}companies/{company_id}/employees/functions",
|
|
32
|
+
params=params)
|
|
33
|
+
|
|
34
|
+
data = self.nmbrs.get_paginated_result(request)
|
|
35
|
+
df = pd.json_normalize(
|
|
36
|
+
data,
|
|
37
|
+
record_path='functions',
|
|
38
|
+
meta=['employeeId']
|
|
39
|
+
)
|
|
40
|
+
except requests.HTTPError as e:
|
|
41
|
+
df = pd.DataFrame()
|
|
42
|
+
|
|
43
|
+
return df
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create(self,
|
|
47
|
+
employee_id: str,
|
|
48
|
+
data: dict):
|
|
49
|
+
|
|
50
|
+
required_fields = ["first_name", "period", "function_id"]
|
|
51
|
+
allowed_fields = {}
|
|
52
|
+
# self.nmbrs.check_fields(data=data, required_fields=required_fields, allowed_fields=list(allowed_fields.keys()))
|
|
53
|
+
|
|
54
|
+
ChildType = self.client.get_type('ns0:Child')
|
|
55
|
+
child = ChildType(
|
|
56
|
+
Id=1, # Use 0 or omit if adding a new child
|
|
57
|
+
Name='Doe',
|
|
58
|
+
FirstName='John',
|
|
59
|
+
Initials='J.D.',
|
|
60
|
+
Gender='male', # Options: 'male', 'female', 'unknown', 'undefined'
|
|
61
|
+
Birthday=datetime(2020, 1, 1) # Using a datetime object
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Make the API call
|
|
65
|
+
result = self.client.service.Children_Insert(
|
|
66
|
+
EmployeeId=employee_id,
|
|
67
|
+
child=child
|
|
68
|
+
)
|
|
69
|
+
print("Child inserted successfully. Result:", result)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:emp="https://api.nmbrs.nl/soap/v3/EmployeeService">
|
|
73
|
+
# <soap:Header>
|
|
74
|
+
# <emp:AuthHeaderWithDomain>
|
|
75
|
+
# <!--Optional:-->
|
|
76
|
+
# <emp:Username>erwin.vink@brynq.com</emp:Username>
|
|
77
|
+
# <!--Optional:-->
|
|
78
|
+
# <emp:Token>cc358715f5c14cda8add964deef99ba3</emp:Token>
|
|
79
|
+
# <!--Optional:-->
|
|
80
|
+
# <emp:Domain>extdev-brynq</emp:Domain>
|
|
81
|
+
# </emp:AuthHeaderWithDomain>
|
|
82
|
+
# </soap:Header>
|
|
83
|
+
# <soap:Body>
|
|
84
|
+
# <emp:Children_Insert>
|
|
85
|
+
# <emp:EmployeeId>11</emp:EmployeeId>
|
|
86
|
+
# <!--Optional:-->
|
|
87
|
+
# <emp:child>
|
|
88
|
+
# <emp:Id>1</emp:Id>
|
|
89
|
+
# <!--Optional:-->
|
|
90
|
+
# <emp:Name>Doe</emp:Name>
|
|
91
|
+
# <!--Optional:-->
|
|
92
|
+
# <emp:FirstName>John</emp:FirstName>
|
|
93
|
+
# <!--Optional:-->
|
|
94
|
+
# <emp:Initials>J.</emp:Initials>
|
|
95
|
+
# <emp:Gender>male</emp:Gender>
|
|
96
|
+
# <emp:Birthday>2020-01-01T00:00:00</emp:Birthday>
|
|
97
|
+
# </emp:child>
|
|
98
|
+
# </emp:Children_Insert>
|
|
99
|
+
# </soap:Body>
|
|
100
|
+
# </soap:Envelope>
|