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.
Files changed (50) hide show
  1. brynq_sdk_nmbrs/__init__.py +226 -0
  2. brynq_sdk_nmbrs/absence.py +124 -0
  3. brynq_sdk_nmbrs/address.py +66 -0
  4. brynq_sdk_nmbrs/bank.py +125 -0
  5. brynq_sdk_nmbrs/children.py +100 -0
  6. brynq_sdk_nmbrs/companies.py +93 -0
  7. brynq_sdk_nmbrs/contract.py +132 -0
  8. brynq_sdk_nmbrs/costcenter.py +166 -0
  9. brynq_sdk_nmbrs/costunit.py +90 -0
  10. brynq_sdk_nmbrs/days.py +137 -0
  11. brynq_sdk_nmbrs/debtors.py +25 -0
  12. brynq_sdk_nmbrs/department.py +122 -0
  13. brynq_sdk_nmbrs/document.py +30 -0
  14. brynq_sdk_nmbrs/employees.py +196 -0
  15. brynq_sdk_nmbrs/employment.py +107 -0
  16. brynq_sdk_nmbrs/function.py +89 -0
  17. brynq_sdk_nmbrs/hours.py +252 -0
  18. brynq_sdk_nmbrs/leave.py +139 -0
  19. brynq_sdk_nmbrs/manager.py +294 -0
  20. brynq_sdk_nmbrs/salaries.py +85 -0
  21. brynq_sdk_nmbrs/salary_tables.py +242 -0
  22. brynq_sdk_nmbrs/schedules.py +84 -0
  23. brynq_sdk_nmbrs/schemas/__init__.py +37 -0
  24. brynq_sdk_nmbrs/schemas/absence.py +61 -0
  25. brynq_sdk_nmbrs/schemas/address.py +76 -0
  26. brynq_sdk_nmbrs/schemas/bank.py +83 -0
  27. brynq_sdk_nmbrs/schemas/contracts.py +60 -0
  28. brynq_sdk_nmbrs/schemas/costcenter.py +91 -0
  29. brynq_sdk_nmbrs/schemas/costunit.py +40 -0
  30. brynq_sdk_nmbrs/schemas/days.py +98 -0
  31. brynq_sdk_nmbrs/schemas/debtor.py +16 -0
  32. brynq_sdk_nmbrs/schemas/department.py +57 -0
  33. brynq_sdk_nmbrs/schemas/employees.py +153 -0
  34. brynq_sdk_nmbrs/schemas/employment.py +48 -0
  35. brynq_sdk_nmbrs/schemas/function.py +50 -0
  36. brynq_sdk_nmbrs/schemas/hours.py +121 -0
  37. brynq_sdk_nmbrs/schemas/leave.py +53 -0
  38. brynq_sdk_nmbrs/schemas/manager.py +126 -0
  39. brynq_sdk_nmbrs/schemas/salary.py +92 -0
  40. brynq_sdk_nmbrs/schemas/schedules.py +96 -0
  41. brynq_sdk_nmbrs/schemas/social_insurance.py +40 -0
  42. brynq_sdk_nmbrs/schemas/wage_tax.py +98 -0
  43. brynq_sdk_nmbrs/schemas/wagecomponents.py +114 -0
  44. brynq_sdk_nmbrs/social_insurance.py +52 -0
  45. brynq_sdk_nmbrs/wage_tax.py +164 -0
  46. brynq_sdk_nmbrs/wagecomponents.py +268 -0
  47. brynq_sdk_nmbrs-2.3.1.dist-info/METADATA +21 -0
  48. brynq_sdk_nmbrs-2.3.1.dist-info/RECORD +50 -0
  49. brynq_sdk_nmbrs-2.3.1.dist-info/WHEEL +5 -0
  50. 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
@@ -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>