brynq-sdk-nmbrs 2.1.0__tar.gz → 2.2.1__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.1.0 → brynq_sdk_nmbrs-2.2.1}/PKG-INFO +1 -1
  2. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/__init__.py +41 -5
  3. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/absence.py +131 -0
  4. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/address.py +3 -3
  5. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/bank.py +6 -4
  6. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/companies.py +39 -9
  7. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/contract.py +10 -6
  8. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/costcenter.py +18 -39
  9. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/costunit.py +6 -4
  10. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/days.py +137 -0
  11. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/department.py +9 -6
  12. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/employees.py +34 -17
  13. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/employment.py +6 -4
  14. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/function.py +4 -32
  15. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/hours.py +44 -38
  16. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/leave.py +139 -0
  17. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/salaries.py +6 -33
  18. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/salary_tables.py +28 -28
  19. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/schedules.py +84 -0
  20. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/__init__.py +12 -9
  21. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/schemas/absence.py +61 -0
  22. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/address.py +1 -31
  23. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/bank.py +16 -17
  24. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/contracts.py +3 -3
  25. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/costcenter.py +20 -18
  26. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/schemas/days.py +96 -0
  27. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/department.py +5 -11
  28. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/employees.py +16 -12
  29. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/function.py +1 -8
  30. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/hours.py +10 -7
  31. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/schemas/leave.py +52 -0
  32. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/salary.py +8 -16
  33. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/schedules.py +9 -28
  34. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/schemas/social_insurance.py +40 -0
  35. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/schemas/wage_tax.py +57 -0
  36. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/social_insurance.py +55 -0
  37. brynq_sdk_nmbrs-2.2.1/brynq_sdk_nmbrs/wage_tax.py +124 -0
  38. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs.egg-info/PKG-INFO +1 -1
  39. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs.egg-info/SOURCES.txt +10 -0
  40. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/setup.py +1 -1
  41. brynq_sdk_nmbrs-2.1.0/brynq_sdk_nmbrs/schedules.py +0 -149
  42. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/children.py +0 -0
  43. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/debtors.py +0 -0
  44. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/document.py +0 -0
  45. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/manager.py +0 -0
  46. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/costunit.py +0 -0
  47. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/debtor.py +0 -0
  48. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/employment.py +0 -0
  49. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/manager.py +0 -0
  50. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/schemas/wagecomponents.py +0 -0
  51. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs/wagecomponents.py +0 -0
  52. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs.egg-info/dependency_links.txt +0 -0
  53. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs.egg-info/not-zip-safe +0 -0
  54. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs.egg-info/requires.txt +0 -0
  55. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/brynq_sdk_nmbrs.egg-info/top_level.txt +0 -0
  56. {brynq_sdk_nmbrs-2.1.0 → brynq_sdk_nmbrs-2.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.0
2
2
  Name: brynq_sdk_nmbrs
3
- Version: 2.1.0
3
+ Version: 2.2.1
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,7 @@ 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
23
27
  from .manager import EmployeeManager, Manager
24
28
  from .salaries import Salaries
25
29
  from .schedules import Schedule
@@ -35,7 +39,7 @@ class Nmbrs(BrynQ):
35
39
  Args:
36
40
  label: The label of the system in BrynQ. legacy
37
41
  debug: Whether to print debug information
38
- 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
39
43
  """
40
44
  self.mock_mode = mock_mode
41
45
  self.debug = debug
@@ -55,10 +59,14 @@ class Nmbrs(BrynQ):
55
59
  xml_huge_tree=True,
56
60
  force_https=True
57
61
  )
58
- self.soap_client = Client(
62
+ self.soap_client_companies = Client(
59
63
  'https://api.nmbrs.nl/soap/v3/CompanyService.asmx?wsdl',
60
64
  settings=self.soap_settings
61
65
  )
66
+ self.soap_client_employees = Client(
67
+ 'https://api.nmbrs.nl/soap/v3/EmployeeService.asmx?wsdl',
68
+ settings=self.soap_settings
69
+ )
62
70
 
63
71
  self.address = Address(self)
64
72
  self.bank = Bank(self)
@@ -67,13 +75,17 @@ class Nmbrs(BrynQ):
67
75
  self.companies = Companies(self)
68
76
  self.contract = Contract(self)
69
77
  self.department = EmployeeDepartment(self)
70
- 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()
71
81
  self.soap_company_ids = self.companies.get_soap_ids()
72
82
  self.employees = Employees(self)
73
83
  self.employment = Employment(self)
74
84
  self.function = EmployeeFunction(self)
75
85
  self.fixed_hours = FixedHours(self)
86
+ self.fixed_days = FixedDays(self)
76
87
  self.variable_hours = VariableHours(self)
88
+ self.variable_days = VariableDays(self)
77
89
  self.manager = Manager(self)
78
90
  self.employee_manager = EmployeeManager(self)
79
91
  self.salaries = Salaries(self)
@@ -84,6 +96,9 @@ class Nmbrs(BrynQ):
84
96
  self.salary_tables = SalaryTables(self)
85
97
  self.salary_scales = SalaryScales(self)
86
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()
87
102
 
88
103
  def _get_request_headers(self):
89
104
  credentials = self.interfaces.credentials.get(system='nmbrs', system_type=self.system_type)
@@ -99,6 +114,7 @@ class Nmbrs(BrynQ):
99
114
  def _get_soap_auth_header(self):
100
115
  """
101
116
  Creates the SOAP authentication header using credentials from initial_credentials.
117
+
102
118
  Returns:
103
119
  AuthHeaderWithDomainType: The authentication header for SOAP requests
104
120
  """
@@ -106,7 +122,7 @@ class Nmbrs(BrynQ):
106
122
  config = initial_credentials.get("config", {})
107
123
 
108
124
  # Get the AuthHeaderWithDomain type from the WSDL
109
- AuthHeaderWithDomainType = self.soap_client.get_element('ns0:AuthHeaderWithDomain')
125
+ AuthHeaderWithDomainType = self.soap_client_companies.get_element('ns0:AuthHeaderWithDomain')
110
126
 
111
127
  # Create the auth header using credentials from config
112
128
  auth_header = AuthHeaderWithDomainType(
@@ -160,3 +176,23 @@ class Nmbrs(BrynQ):
160
176
  df.columns = map(camel_to_snake_case, df.columns)
161
177
 
162
178
  return df
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
@@ -2,17 +2,19 @@ import pandas as pd
2
2
  import requests
3
3
  from dateutil.utils import today
4
4
  from requests import HTTPError
5
- from typing import Dict, Any
5
+ from typing import Dict, Any, TYPE_CHECKING
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
+ if TYPE_CHECKING:
12
+ from brynq_sdk_nmbrs import Nmbrs
11
13
 
12
14
 
13
15
  class EmployeeCostcenter:
14
16
  def __init__(self, nmbrs):
15
- self.nmbrs = nmbrs
17
+ self.nmbrs: Nmbrs = nmbrs
16
18
 
17
19
  def get(self,
18
20
  created_from: str = None,
@@ -46,35 +48,6 @@ class EmployeeCostcenter:
46
48
 
47
49
  return df
48
50
 
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
51
  def update(self, employee_id: str, data: Dict[str, Any]):
79
52
  """
80
53
  Update a costcenter for an employee using Pydantic validation.
@@ -87,18 +60,22 @@ class EmployeeCostcenter:
87
60
  Returns:
88
61
  Response from the API
89
62
  """
63
+ # this is the Nmbrs GUID that is returned after creating an employee, for some reason also included in body here.
64
+ data['employee_id'] = employee_id
65
+ data['default'] = True
90
66
  # Validate with Pydantic model - this will raise an error if required fields are missing
91
- costcenter_model = EmployeeCostcenterUpdate(**data)
67
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, EmployeeCostcenterUpdate)
68
+ costcenter_model = EmployeeCostcenterUpdate(**nested_data)
92
69
 
93
70
  if self.nmbrs.mock_mode:
94
71
  return costcenter_model
95
72
 
96
73
  # Convert validated model to dict for API payload
97
- payload = costcenter_model.dict(exclude_none=True)
74
+ payload = costcenter_model.model_dump(exclude_none=True, by_alias=True)
98
75
 
99
76
  # Send request
100
77
  resp = self.nmbrs.session.put(
101
- url=f"{self.nmbrs.base_url}employees/{employee_id}/costcenter",
78
+ url=f"{self.nmbrs.base_url}employees/{employee_id}/employeecostcenter",
102
79
  json=payload,
103
80
  timeout=self.nmbrs.timeout
104
81
  )
@@ -141,13 +118,14 @@ class Costcenter:
141
118
  Response from the API
142
119
  """
143
120
  # Validate with Pydantic model - this will raise an error if required fields are missing
144
- costcenter_model = CostcenterCreate(**data)
121
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, CostcenterCreate)
122
+ costcenter_model = CostcenterCreate(**nested_data)
145
123
 
146
124
  if self.nmbrs.mock_mode:
147
125
  return costcenter_model
148
126
 
149
127
  # Convert validated model to dict for API payload
150
- payload = costcenter_model.dict(exclude_none=True)
128
+ payload = costcenter_model.model_dump(exclude_none=True, by_alias=True)
151
129
 
152
130
  # Send request
153
131
  resp = self.nmbrs.session.post(
@@ -170,13 +148,14 @@ class Costcenter:
170
148
  Response from the API
171
149
  """
172
150
  # Validate with Pydantic model - this will raise an error if required fields are missing
173
- costcenter_model = CostcenterUpdate(**data)
151
+ nested_data = self.nmbrs.flat_dict_to_nested_dict(data, CostcenterUpdate)
152
+ costcenter_model = CostcenterUpdate(**nested_data)
174
153
 
175
154
  if self.nmbrs.mock_mode:
176
155
  return costcenter_model
177
156
 
178
157
  # Convert validated model to dict for API payload
179
- payload = costcenter_model.dict(exclude_none=True)
158
+ payload = costcenter_model.model_dump(exclude_none=True, by_alias=True)
180
159
 
181
160
  # Send request
182
161
  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(