brynq-sdk-nmbrs 2.0.1__tar.gz → 2.2.0__tar.gz

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 (56) hide show
  1. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/PKG-INFO +1 -1
  2. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/__init__.py +46 -9
  3. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/absence.py +131 -0
  4. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/address.py +3 -3
  5. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/bank.py +6 -4
  6. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/companies.py +39 -9
  7. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/contract.py +10 -6
  8. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/costcenter.py +14 -37
  9. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/costunit.py +6 -4
  10. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/days.py +137 -0
  11. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/department.py +9 -6
  12. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/employees.py +34 -17
  13. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/employment.py +6 -4
  14. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/function.py +4 -32
  15. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/hours.py +44 -38
  16. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/leave.py +139 -0
  17. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/manager.py +294 -0
  18. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/salaries.py +6 -33
  19. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/salary_tables.py +28 -28
  20. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/schedules.py +84 -0
  21. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/__init__.py +14 -8
  22. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/schemas/absence.py +61 -0
  23. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/address.py +1 -31
  24. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/bank.py +16 -17
  25. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/contracts.py +3 -3
  26. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/costcenter.py +18 -16
  27. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/schemas/days.py +96 -0
  28. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/department.py +5 -11
  29. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/employees.py +16 -12
  30. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/function.py +1 -8
  31. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/hours.py +10 -7
  32. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/schemas/leave.py +52 -0
  33. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/schemas/manager.py +123 -0
  34. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/salary.py +8 -16
  35. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/schedules.py +9 -28
  36. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/schemas/social_insurance.py +40 -0
  37. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/schemas/wage_tax.py +57 -0
  38. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/social_insurance.py +55 -0
  39. brynq_sdk_nmbrs-2.2.0/brynq_sdk_nmbrs/wage_tax.py +124 -0
  40. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs.egg-info/PKG-INFO +1 -1
  41. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs.egg-info/SOURCES.txt +12 -0
  42. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/setup.py +1 -1
  43. brynq_sdk_nmbrs-2.0.1/brynq_sdk_nmbrs/schedules.py +0 -149
  44. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/children.py +0 -0
  45. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/debtors.py +0 -0
  46. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/document.py +0 -0
  47. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/costunit.py +0 -0
  48. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/debtor.py +0 -0
  49. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/employment.py +0 -0
  50. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/schemas/wagecomponents.py +0 -0
  51. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs/wagecomponents.py +0 -0
  52. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs.egg-info/dependency_links.txt +0 -0
  53. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs.egg-info/not-zip-safe +0 -0
  54. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs.egg-info/requires.txt +0 -0
  55. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/brynq_sdk_nmbrs.egg-info/top_level.txt +0 -0
  56. {brynq_sdk_nmbrs-2.0.1 → brynq_sdk_nmbrs-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.0
2
2
  Name: brynq_sdk_nmbrs
3
- Version: 2.0.1
3
+ Version: 2.2.0
4
4
  Summary: Nmbrs wrapper from BrynQ
5
5
  Home-page: UNKNOWN
6
6
  Author: BrynQ
@@ -1,9 +1,12 @@
1
1
  import warnings
2
- from typing import Union, List, Literal, Optional
2
+ from typing import Union, List, Literal, Optional, get_args, get_origin
3
3
  import re
4
4
  import pandas as pd
5
+ from pydantic import BaseModel
5
6
  import requests
6
7
  from brynq_sdk_brynq import BrynQ
8
+ from .absence import Absence
9
+ from .wage_tax import WageTax
7
10
  from .address import Address
8
11
  from .bank import Bank
9
12
  from zeep import Client, Settings
@@ -20,6 +23,8 @@ from .salary_tables import SalaryTables, SalaryScales, SalarySteps
20
23
  from .employment import Employment
21
24
  from .function import EmployeeFunction
22
25
  from .hours import VariableHours, FixedHours
26
+ from .days import VariableDays, FixedDays
27
+ from .manager import EmployeeManager, Manager
23
28
  from .salaries import Salaries
24
29
  from .schedules import Schedule
25
30
  from .wagecomponents import EmployeeFixedWageComponents, EmployeeVariableWageComponents
@@ -34,7 +39,7 @@ class Nmbrs(BrynQ):
34
39
  Args:
35
40
  label: The label of the system in BrynQ. legacy
36
41
  debug: Whether to print debug information
37
- mock_mode: If true, data will NOT be send to Nmbrs but only be tested for validity against Pydantic schemas
42
+ 11 mock_mode: If true, data will NOT be sent to Nmbrs but only be tested for validity against Pydantic schemas
38
43
  """
39
44
  self.mock_mode = mock_mode
40
45
  self.debug = debug
@@ -54,10 +59,14 @@ class Nmbrs(BrynQ):
54
59
  xml_huge_tree=True,
55
60
  force_https=True
56
61
  )
57
- self.soap_client = Client(
62
+ self.soap_client_companies = Client(
58
63
  'https://api.nmbrs.nl/soap/v3/CompanyService.asmx?wsdl',
59
64
  settings=self.soap_settings
60
65
  )
66
+ self.soap_client_employees = Client(
67
+ 'https://api.nmbrs.nl/soap/v3/EmployeeService.asmx?wsdl',
68
+ settings=self.soap_settings
69
+ )
61
70
 
62
71
  self.address = Address(self)
63
72
  self.bank = Bank(self)
@@ -66,13 +75,19 @@ class Nmbrs(BrynQ):
66
75
  self.companies = Companies(self)
67
76
  self.contract = Contract(self)
68
77
  self.department = EmployeeDepartment(self)
69
- self.company_ids = self.companies.get()['companyId']
78
+ debtors, _ = self.debtor.get()
79
+ self.debtor_ids = debtors['debtor_id'].to_list()
80
+ self.company_ids = self.companies.get()['companyId'].to_list()
70
81
  self.soap_company_ids = self.companies.get_soap_ids()
71
82
  self.employees = Employees(self)
72
83
  self.employment = Employment(self)
73
84
  self.function = EmployeeFunction(self)
74
85
  self.fixed_hours = FixedHours(self)
86
+ self.fixed_days = FixedDays(self)
75
87
  self.variable_hours = VariableHours(self)
88
+ self.variable_days = VariableDays(self)
89
+ self.manager = Manager(self)
90
+ self.employee_manager = EmployeeManager(self)
76
91
  self.salaries = Salaries(self)
77
92
  self.schedule = Schedule(self)
78
93
  self.fixed_wagecomponents = EmployeeFixedWageComponents(self)
@@ -81,6 +96,9 @@ class Nmbrs(BrynQ):
81
96
  self.salary_tables = SalaryTables(self)
82
97
  self.salary_scales = SalaryScales(self)
83
98
  self.salary_steps = SalarySteps(self)
99
+ self.wage_tax = WageTax(self)
100
+ self.absence = Absence(self)
101
+ self.current_period = self.companies.get_current_period()
84
102
 
85
103
  def _get_request_headers(self):
86
104
  credentials = self.interfaces.credentials.get(system='nmbrs', system_type=self.system_type)
@@ -96,23 +114,23 @@ class Nmbrs(BrynQ):
96
114
  def _get_soap_auth_header(self):
97
115
  """
98
116
  Creates the SOAP authentication header using credentials from initial_credentials.
99
-
117
+
100
118
  Returns:
101
119
  AuthHeaderWithDomainType: The authentication header for SOAP requests
102
120
  """
103
121
  initial_credentials = self.get_system_credential(system='nmbrs', label='bob')
104
122
  config = initial_credentials.get("config", {})
105
-
123
+
106
124
  # Get the AuthHeaderWithDomain type from the WSDL
107
- AuthHeaderWithDomainType = self.soap_client.get_element('ns0:AuthHeaderWithDomain')
108
-
125
+ AuthHeaderWithDomainType = self.soap_client_companies.get_element('ns0:AuthHeaderWithDomain')
126
+
109
127
  # Create the auth header using credentials from config
110
128
  auth_header = AuthHeaderWithDomainType(
111
129
  Username=config.get("soap_api_username"),
112
130
  Token=config.get("soap_api_token"),
113
131
  Domain=config.get("soap_api_domain")
114
132
  )
115
-
133
+
116
134
  return auth_header
117
135
 
118
136
  def get_paginated_result(self, request: requests.Request) -> List:
@@ -159,3 +177,22 @@ class Nmbrs(BrynQ):
159
177
 
160
178
  return df
161
179
 
180
+ def flat_dict_to_nested_dict(self, flat_dict: dict, model: BaseModel) -> dict:
181
+ nested = {}
182
+ for name, field in model.model_fields.items():
183
+ key_in_input = name # Original model field name as key in flat_dict
184
+ alias = field.alias or name
185
+ if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
186
+ nested[alias] = self.flat_dict_to_nested_dict(flat_dict, field.annotation)
187
+ elif any(isinstance(item, type) and issubclass(item, BaseModel) for item in get_args(field.annotation)):
188
+ # get the basemodel class from the list
189
+ nested_model_name = [item for item in get_args(field.annotation) if isinstance(item, type) and issubclass(item, BaseModel)][0]
190
+ origin = get_origin(field.annotation)
191
+ if origin in (list, List):
192
+ nested[alias] = [self.flat_dict_to_nested_dict(flat_dict, nested_model_name)]
193
+ else:
194
+ nested[alias] = self.flat_dict_to_nested_dict(flat_dict, nested_model_name)
195
+ else:
196
+ if key_in_input in flat_dict:
197
+ nested[alias] = flat_dict[key_in_input]
198
+ return nested
@@ -0,0 +1,131 @@
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 salary tables 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
+
55
+ # Get the auth header using the centralized method
56
+ auth_header = self.nmbrs._get_soap_auth_header()
57
+
58
+ if employee_id is None:
59
+ response = self.soap_client_employees.service.Absence_GetAll_AllEmployeesByCompany(
60
+ CompanyId=company_id,
61
+ _soapheaders=[auth_header]
62
+ )
63
+ else:
64
+ # Make SOAP request with the proper header structure
65
+ response = self.soap_client_employees.service.Absence_GetList(
66
+ EmployeeId=employee_id,
67
+ _soapheaders=[auth_header]
68
+ )
69
+
70
+ # Convert response to DataFrame
71
+ if response:
72
+ # Convert Zeep objects to Python dictionaries
73
+ serialized_response = serialize_object(response)
74
+
75
+ # Convert to list if it's not already
76
+ if not isinstance(serialized_response, list):
77
+ serialized_response = [serialized_response]
78
+
79
+ # Convert to DataFrame
80
+ df = pd.DataFrame(serialized_response)
81
+
82
+ return df
83
+ else:
84
+ return pd.DataFrame()
85
+
86
+ except Fault as e:
87
+ raise Exception(f"SOAP request failed: {str(e)}")
88
+ except Exception as e:
89
+ raise Exception(f"Failed to get salary tables: {str(e)}")
90
+
91
+ def create(self, data: Dict[str, Any]) -> pd.DataFrame:
92
+ try:
93
+ absence_model = AbsenceCreate(**data)
94
+
95
+ if self.nmbrs.mock_mode:
96
+ return absence_model
97
+
98
+ # Get the auth header using the centralized method
99
+ auth_header = self.nmbrs._get_soap_auth_header()
100
+
101
+ # Use the model's built-in SOAP conversion method
102
+ absence_settings = absence_model.to_soap_settings(self.nmbrs.soap_client_employees)
103
+
104
+ # Make SOAP request with clean, simple call
105
+ response = self.nmbrs.soap_client_employees.service.Absence_Create(
106
+ EmployeeId=absence_model.employee_id,
107
+ Dossier=absence_model.new_dossier,
108
+ Absence=absence_settings,
109
+ _soapheaders=[auth_header]
110
+ )
111
+
112
+ # Convert response to DataFrame
113
+ if response:
114
+ # Convert Zeep objects to Python dictionaries
115
+ serialized_response = serialize_object(response)
116
+
117
+ # Convert to list if it's not already
118
+ if not isinstance(serialized_response, list):
119
+ serialized_response = [serialized_response]
120
+
121
+ # Convert to DataFrame
122
+ df = pd.DataFrame(serialized_response)
123
+
124
+ return df
125
+ else:
126
+ return pd.DataFrame()
127
+
128
+ except Fault as e:
129
+ raise Exception(f"SOAP request failed: {str(e)}")
130
+ except Exception as e:
131
+ raise Exception(f"Failed to update WageTax: {str(e)}")
@@ -48,13 +48,14 @@ class Address:
48
48
  Response from the API
49
49
  """
50
50
  # Validate with Pydantic model - this will raise an error if required fields are missing
51
- address_model = AddressCreate(**data)
51
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, AddressCreate)
52
+ address_model = AddressCreate(**nested_data)
52
53
 
53
54
  if self.nmbrs.mock_mode:
54
55
  return address_model
55
56
 
56
57
  # Convert validated model to dict for API payload
57
- payload = address_model.dict(exclude_none=True)
58
+ payload = address_model.model_dump(exclude_none=True, by_alias=True)
58
59
 
59
60
  # Send request
60
61
  resp = self.nmbrs.session.post(
@@ -63,4 +64,3 @@ class Address:
63
64
  timeout=self.nmbrs.timeout
64
65
  )
65
66
  return resp
66
-
@@ -54,13 +54,14 @@ class Bank:
54
54
  Response from the API
55
55
  """
56
56
  # Validate with Pydantic model - this will raise an error if required fields are missing
57
- bank_model = BankCreate(**data)
57
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, BankCreate)
58
+ bank_model = BankCreate(**nested_data)
58
59
 
59
60
  if self.nmbrs.mock_mode:
60
61
  return bank_model
61
62
 
62
63
  # Convert validated model to dict for API payload
63
- payload = bank_model.dict(exclude_none=True)
64
+ payload = bank_model.model_dump(exclude_none=True, by_alias=True)
64
65
 
65
66
  # Send request
66
67
  resp = self.nmbrs.session.post(
@@ -82,13 +83,14 @@ class Bank:
82
83
  Response from the API
83
84
  """
84
85
  # Validate with Pydantic model - this will raise an error if required fields are missing
85
- bank_model = BankUpdate(**data)
86
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, BankUpdate)
87
+ bank_model = BankUpdate(**nested_data)
86
88
 
87
89
  if self.nmbrs.mock_mode:
88
90
  return bank_model
89
91
 
90
92
  # Convert validated model to dict for API payload
91
- payload = bank_model.dict(exclude_none=True)
93
+ payload = bank_model.model_dump(exclude_none=True, by_alias=True)
92
94
 
93
95
  # Send request
94
96
  resp = self.nmbrs.session.put(
@@ -1,6 +1,8 @@
1
1
  import pandas as pd
2
2
  import requests
3
3
  import logging
4
+
5
+ from .leave import LeaveGroup
4
6
  from .costcenter import Costcenter
5
7
  from .costunit import Costunit
6
8
  from .hours import Hours
@@ -17,22 +19,34 @@ class Companies:
17
19
  self.costunits = Costunit(nmbrs)
18
20
  self.hours = Hours(nmbrs)
19
21
  self.banks = Bank(nmbrs)
20
- self.soap_client = nmbrs.soap_client
22
+ self.soap_client_companies = nmbrs.soap_client_companies
21
23
  self.logger = logging.getLogger(__name__)
24
+ self.leave_groups = LeaveGroup(nmbrs)
22
25
 
23
26
  def get(self) -> pd.DataFrame:
24
- request = requests.Request(method='GET',
25
- url=f"{self.nmbrs.base_url}companies")
26
- data = self.nmbrs.get_paginated_result(request)
27
- df = pd.DataFrame(data)
28
- # TODO: add validation for rename
27
+ try:
28
+ request = requests.Request(method='GET',
29
+ url=f"{self.nmbrs.base_url}companies")
30
+ data = self.nmbrs.get_paginated_result(request)
31
+ df = pd.DataFrame(data)
32
+ except requests.HTTPError as e:
33
+ if e.response.status_code == 403 and e.response.json().get("title") == "ForbiddenMultiDebtor.":
34
+ df = pd.DataFrame()
35
+ for debtor_id in self.nmbrs.debtor_ids:
36
+ request = requests.Request(method='GET',
37
+ url=f"{self.nmbrs.base_url}debtors/{debtor_id}/companies")
38
+ data = self.nmbrs.get_paginated_result(request)
39
+ df = pd.concat([df, pd.DataFrame(data)])
40
+ else:
41
+ raise e
42
+ # TODO: add validation
29
43
 
30
44
  return df
31
45
 
32
46
  def get_soap_ids(self) -> pd.DataFrame:
33
47
  """
34
48
  Get all companies using the SOAP API.
35
-
49
+
36
50
  Returns:
37
51
  pd.DataFrame: DataFrame containing all companies
38
52
  """
@@ -41,10 +55,10 @@ class Companies:
41
55
  auth_header = self.nmbrs._get_soap_auth_header()
42
56
 
43
57
  # Make SOAP request with the proper header structure
44
- response = self.soap_client.service.List_GetAll(
58
+ response = self.soap_client_companies.service.List_GetAll(
45
59
  _soapheaders=[auth_header]
46
60
  )
47
-
61
+
48
62
  # Convert response to DataFrame
49
63
  if response:
50
64
  # Convert Zeep objects to Python dictionaries
@@ -64,3 +78,19 @@ class Companies:
64
78
  except Exception as e:
65
79
  self.logger.exception("Exception occurred:")
66
80
  raise Exception(f"Failed to get companies: {str(e)}")
81
+
82
+ def get_current_period(self) -> int:
83
+ try:
84
+ df = pd.DataFrame()
85
+ for company_id in self.nmbrs.company_ids:
86
+ request = requests.Request(method='GET',
87
+ url=f"{self.nmbrs.base_url}companies/{company_id}/period")
88
+ data = self.nmbrs.get_paginated_result(request)
89
+ df_temp = pd.json_normalize(data)
90
+ df_temp['company_id'] = company_id
91
+ df = pd.concat([df, df_temp])
92
+
93
+ return df
94
+ except Exception as e:
95
+ self.logger.exception("Exception occurred:")
96
+ raise Exception(f"Failed to get current period: {str(e)}")
@@ -58,18 +58,20 @@ class Contract:
58
58
  Response from the API
59
59
  """
60
60
  # Validate with Pydantic model - this will raise an error if required fields are missing
61
- contract_model = ContractCreate(**data)
61
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, ContractCreate)
62
+ contract_model = ContractCreate(**nested_data)
62
63
 
63
64
  if self.nmbrs.mock_mode:
64
65
  return contract_model
65
66
 
66
67
  # Convert validated model to dict for API payload
67
- payload = contract_model.dict(exclude_none=True)
68
+ payload = contract_model.model_dump_json(exclude_none=True, by_alias=True)
68
69
 
69
70
  # Send request
70
71
  resp = self.nmbrs.session.post(
71
72
  url=f"{self.nmbrs.base_url}employees/{employee_id}/contract",
72
- json=payload,
73
+ data=payload,
74
+ headers={"Content-Type": "application/json"},
73
75
  timeout=self.nmbrs.timeout
74
76
  )
75
77
  return resp
@@ -87,18 +89,20 @@ class Contract:
87
89
  Response from the API
88
90
  """
89
91
  # Validate with Pydantic model - this will raise an error if required fields are missing
90
- contract_model = ContractUpdate(**data)
92
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, ContractUpdate)
93
+ contract_model = ContractUpdate(**nested_data)
91
94
 
92
95
  if self.nmbrs.mock_mode:
93
96
  return contract_model
94
97
 
95
98
  # Convert validated model to dict for API payload
96
- payload = contract_model.dict(exclude_none=True)
99
+ payload = contract_model.model_dump_json(exclude_none=True, by_alias=True)
97
100
 
98
101
  # Send request
99
102
  resp = self.nmbrs.session.put(
100
103
  url=f"{self.nmbrs.base_url}employees/{employee_id}/contract",
101
- json=payload,
104
+ data=payload,
105
+ headers={"Content-Type": "application/json"},
102
106
  timeout=self.nmbrs.timeout
103
107
  )
104
108
  return resp
@@ -6,7 +6,7 @@ from typing import Dict, Any
6
6
 
7
7
  from brynq_sdk_functions import Functions
8
8
  from .schemas.costcenter import CostcenterGet, EmployeeCostcenterGet
9
- from .schemas.costcenter import EmployeeCostcenterCreate, EmployeeCostcenterUpdate, EmployeeCostcenterDelete
9
+ from .schemas.costcenter import EmployeeCostcenterUpdate, EmployeeCostcenterDelete
10
10
  from .schemas.costcenter import CostcenterCreate, CostcenterUpdate, CostcenterDelete
11
11
 
12
12
 
@@ -46,35 +46,6 @@ class EmployeeCostcenter:
46
46
 
47
47
  return df
48
48
 
49
- def create(self, employee_id: str, data: Dict[str, Any]):
50
- """
51
- Create a new costcenter for an employee using Pydantic validation.
52
-
53
- Args:
54
- employee_id: The ID of the employee
55
- data: Dictionary containing costcenter data with fields matching
56
- the EmployeeCostcenterCreate schema (using camelCase field names)
57
-
58
- Returns:
59
- Response from the API
60
- """
61
- # Validate with Pydantic model - this will raise an error if required fields are missing
62
- costcenter_model = EmployeeCostcenterCreate(**data)
63
-
64
- if self.nmbrs.mock_mode:
65
- return costcenter_model
66
-
67
- # Convert validated model to dict for API payload
68
- payload = costcenter_model.dict(exclude_none=True)
69
-
70
- # Send request
71
- resp = self.nmbrs.session.post(
72
- url=f"{self.nmbrs.base_url}employees/{employee_id}/costcenter",
73
- json=payload,
74
- timeout=self.nmbrs.timeout
75
- )
76
- return resp
77
-
78
49
  def update(self, employee_id: str, data: Dict[str, Any]):
79
50
  """
80
51
  Update a costcenter for an employee using Pydantic validation.
@@ -87,18 +58,22 @@ class EmployeeCostcenter:
87
58
  Returns:
88
59
  Response from the API
89
60
  """
61
+ # this is the Nmbrs GUID that is returned after creating an employee, for some reason also included in body here.
62
+ data['employee_id'] = employee_id
63
+ data['default'] = True
90
64
  # Validate with Pydantic model - this will raise an error if required fields are missing
91
- costcenter_model = EmployeeCostcenterUpdate(**data)
65
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, EmployeeCostcenterUpdate)
66
+ costcenter_model = EmployeeCostcenterUpdate(**nested_data)
92
67
 
93
68
  if self.nmbrs.mock_mode:
94
69
  return costcenter_model
95
70
 
96
71
  # Convert validated model to dict for API payload
97
- payload = costcenter_model.dict(exclude_none=True)
72
+ payload = costcenter_model.model_dump(exclude_none=True, by_alias=True)
98
73
 
99
74
  # Send request
100
75
  resp = self.nmbrs.session.put(
101
- url=f"{self.nmbrs.base_url}employees/{employee_id}/costcenter",
76
+ url=f"{self.nmbrs.base_url}employees/{employee_id}/employeecostcenter",
102
77
  json=payload,
103
78
  timeout=self.nmbrs.timeout
104
79
  )
@@ -141,13 +116,14 @@ class Costcenter:
141
116
  Response from the API
142
117
  """
143
118
  # Validate with Pydantic model - this will raise an error if required fields are missing
144
- costcenter_model = CostcenterCreate(**data)
119
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, CostcenterCreate)
120
+ costcenter_model = CostcenterCreate(**nested_data)
145
121
 
146
122
  if self.nmbrs.mock_mode:
147
123
  return costcenter_model
148
124
 
149
125
  # Convert validated model to dict for API payload
150
- payload = costcenter_model.dict(exclude_none=True)
126
+ payload = costcenter_model.model_dump(exclude_none=True, by_alias=True)
151
127
 
152
128
  # Send request
153
129
  resp = self.nmbrs.session.post(
@@ -170,13 +146,14 @@ class Costcenter:
170
146
  Response from the API
171
147
  """
172
148
  # Validate with Pydantic model - this will raise an error if required fields are missing
173
- costcenter_model = CostcenterUpdate(**data)
149
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, CostcenterUpdate)
150
+ costcenter_model = CostcenterUpdate(**nested_data)
174
151
 
175
152
  if self.nmbrs.mock_mode:
176
153
  return costcenter_model
177
154
 
178
155
  # Convert validated model to dict for API payload
179
- payload = costcenter_model.dict(exclude_none=True)
156
+ payload = costcenter_model.model_dump(exclude_none=True, by_alias=True)
180
157
 
181
158
  # Send request
182
159
  resp = self.nmbrs.session.put(
@@ -42,13 +42,14 @@ class Costunit:
42
42
  Response from the API
43
43
  """
44
44
  # Validate with Pydantic model - this will raise an error if required fields are missing
45
- costunit_model = CostunitCreate(**data)
45
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, CostunitCreate)
46
+ costunit_model = CostunitCreate(**nested_data)
46
47
 
47
48
  if self.nmbrs.mock_mode:
48
49
  return costunit_model
49
50
 
50
51
  # Convert validated model to dict for API payload
51
- payload = costunit_model.dict(exclude_none=True)
52
+ payload = costunit_model.model_dump(exclude_none=True, by_alias=True)
52
53
 
53
54
  # Send request
54
55
  resp = self.nmbrs.session.post(
@@ -71,13 +72,14 @@ class Costunit:
71
72
  Response from the API
72
73
  """
73
74
  # Validate with Pydantic model - this will raise an error if required fields are missing
74
- costunit_model = CostunitUpdate(**data)
75
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, CostunitUpdate)
76
+ costunit_model = CostunitUpdate(**nested_data)
75
77
 
76
78
  if self.nmbrs.mock_mode:
77
79
  return costunit_model
78
80
 
79
81
  # Convert validated model to dict for API payload
80
- payload = costunit_model.dict(exclude_none=True)
82
+ payload = costunit_model.model_dump(exclude_none=True, by_alias=True)
81
83
 
82
84
  # Send request
83
85
  resp = self.nmbrs.session.put(