brynq-sdk-zenegy 1.3.3__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_zenegy/__init__.py +23 -0
- brynq_sdk_zenegy/absence.py +100 -0
- brynq_sdk_zenegy/companies.py +43 -0
- brynq_sdk_zenegy/cost_center.py +80 -0
- brynq_sdk_zenegy/departments.py +77 -0
- brynq_sdk_zenegy/employee_documents.py +40 -0
- brynq_sdk_zenegy/employees.py +301 -0
- brynq_sdk_zenegy/global_value_sets.py +260 -0
- brynq_sdk_zenegy/global_values.py +265 -0
- brynq_sdk_zenegy/paychecks.py +119 -0
- brynq_sdk_zenegy/payroll.py +117 -0
- brynq_sdk_zenegy/payslips.py +43 -0
- brynq_sdk_zenegy/pensions.py +118 -0
- brynq_sdk_zenegy/schemas/__init__.py +30 -0
- brynq_sdk_zenegy/schemas/absences.py +393 -0
- brynq_sdk_zenegy/schemas/companies.py +42 -0
- brynq_sdk_zenegy/schemas/company_cost_centers.py +48 -0
- brynq_sdk_zenegy/schemas/company_departments.py +147 -0
- brynq_sdk_zenegy/schemas/employee_documents.py +30 -0
- brynq_sdk_zenegy/schemas/employee_pay_checks.py +169 -0
- brynq_sdk_zenegy/schemas/employee_pensions.py +140 -0
- brynq_sdk_zenegy/schemas/employees.py +2372 -0
- brynq_sdk_zenegy/schemas/global_value_sets.py +185 -0
- brynq_sdk_zenegy/schemas/global_values.py +433 -0
- brynq_sdk_zenegy/schemas/payrolls.py +134 -0
- brynq_sdk_zenegy/schemas/payslips.py +32 -0
- brynq_sdk_zenegy/schemas/supplements_and_deductions_rates.py +189 -0
- brynq_sdk_zenegy/supplements_and_deductions_rates.py +71 -0
- brynq_sdk_zenegy/zenegy.py +221 -0
- brynq_sdk_zenegy-1.3.3.dist-info/METADATA +16 -0
- brynq_sdk_zenegy-1.3.3.dist-info/RECORD +33 -0
- brynq_sdk_zenegy-1.3.3.dist-info/WHEEL +5 -0
- brynq_sdk_zenegy-1.3.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brynq SDK for Zenegy API integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .absence import Absences
|
|
6
|
+
from .companies import Companies
|
|
7
|
+
from .cost_center import CostCenter
|
|
8
|
+
from .departments import CompanyDepartments
|
|
9
|
+
from .employees import Employees
|
|
10
|
+
from .employee_documents import EmployeeDocuments
|
|
11
|
+
from .global_values import GlobalValues
|
|
12
|
+
from .global_value_sets import GlobalValueSets
|
|
13
|
+
from .paychecks import PayChecks
|
|
14
|
+
from .payroll import Payrolls
|
|
15
|
+
from .payslips import Payslips
|
|
16
|
+
from .pensions import Pensions
|
|
17
|
+
from .supplements_and_deductions_rates import SupplementsAndDeductionsRates
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
'Absences', 'Companies', 'CostCenter', 'CompanyDepartments', 'Employees',
|
|
21
|
+
'EmployeeDocuments', 'GlobalValues', 'GlobalValueSets', 'PayChecks', 'Payrolls',
|
|
22
|
+
'Payslips', 'Pensions', 'SupplementsAndDeductionsRates'
|
|
23
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from .schemas.absences import (CreateAbsenceRequest,
|
|
2
|
+
UpdateAbsenceRequest,
|
|
3
|
+
AbsenceGet)
|
|
4
|
+
import requests
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
from brynq_sdk_functions import Functions
|
|
7
|
+
from typing import Dict, Any, List, Tuple
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
class Absences:
|
|
11
|
+
"""
|
|
12
|
+
Handles all absence-related operations in Zenegy API
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, zenegy):
|
|
16
|
+
"""
|
|
17
|
+
Initialize the Absences class.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
zenegy: The Zenegy instance to use for API calls
|
|
21
|
+
"""
|
|
22
|
+
self.zenegy = zenegy
|
|
23
|
+
self.endpoint = f"api/companies/{self.zenegy.company_uid}/absence"
|
|
24
|
+
|
|
25
|
+
def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
26
|
+
"""
|
|
27
|
+
GetAbsenceDaysPerCompany
|
|
28
|
+
Returns:
|
|
29
|
+
DataFrame with absence information
|
|
30
|
+
"""
|
|
31
|
+
endpoint = f"api/companies/{self.zenegy.company_uid}/absence"
|
|
32
|
+
try:
|
|
33
|
+
# Make the API request and get raw response
|
|
34
|
+
content = self.zenegy.get(endpoint=endpoint)
|
|
35
|
+
|
|
36
|
+
# Get data from response
|
|
37
|
+
data = content.get("data", [])
|
|
38
|
+
if data:
|
|
39
|
+
# Normalize the data
|
|
40
|
+
df = pd.json_normalize(
|
|
41
|
+
data,
|
|
42
|
+
sep='__'
|
|
43
|
+
)
|
|
44
|
+
if df.empty:
|
|
45
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
46
|
+
# Validate data using schema
|
|
47
|
+
valid_data, invalid_data = Functions.validate_data(df, AbsenceGet)
|
|
48
|
+
return valid_data, invalid_data
|
|
49
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
50
|
+
except Exception as e:
|
|
51
|
+
raise Exception(f"Failed to retrieve absences: {str(e)}") from e
|
|
52
|
+
|
|
53
|
+
def create(self, data: Dict[str, Any]) -> requests.Response:
|
|
54
|
+
"""
|
|
55
|
+
Create
|
|
56
|
+
Args:
|
|
57
|
+
data (Dict[str, Any]): The data
|
|
58
|
+
Returns:
|
|
59
|
+
requests.Response: The API response
|
|
60
|
+
"""
|
|
61
|
+
# Validate the data using Pydantic
|
|
62
|
+
try:
|
|
63
|
+
valid_data = CreateAbsenceRequest(**data)
|
|
64
|
+
req_body = valid_data.model_dump(by_alias=True, mode='json',exclude_none=True)
|
|
65
|
+
response = self.zenegy.post(endpoint=self.endpoint, json=req_body)
|
|
66
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
67
|
+
return response
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise Exception(f"Failed to create absence: {str(e)}")
|
|
70
|
+
|
|
71
|
+
def delete(self, absence_uid: UUID) -> requests.Response:
|
|
72
|
+
endpoint = f"{self.endpoint}/{absence_uid}"
|
|
73
|
+
try:
|
|
74
|
+
response = self.zenegy.delete(endpoint=endpoint)
|
|
75
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
76
|
+
return response
|
|
77
|
+
except Exception as e:
|
|
78
|
+
raise Exception(f"Failed to delete absence: {str(e)}")
|
|
79
|
+
|
|
80
|
+
def update(self, absence_uid: UUID, data: Dict[str, Any]) -> requests.Response:
|
|
81
|
+
"""
|
|
82
|
+
Update an existing absence record.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
absence_uid (UUID): The absence uid to update
|
|
86
|
+
data (Dict[str, Any]): The updated absence data
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
requests.Response: The response from the API
|
|
90
|
+
"""
|
|
91
|
+
# Validate the data using Pydantic
|
|
92
|
+
try:
|
|
93
|
+
valida_data = UpdateAbsenceRequest(**data)
|
|
94
|
+
req_body = valida_data.model_dump(by_alias=True, mode='json', exclude_none=True)
|
|
95
|
+
endpoint = f"{self.endpoint}/{absence_uid}"
|
|
96
|
+
response = self.zenegy.put(endpoint=endpoint, json=req_body)
|
|
97
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
98
|
+
return response
|
|
99
|
+
except Exception as e:
|
|
100
|
+
raise Exception(f"Failed to update absences: {str(e)}")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from .schemas.companies import CompaniesGet
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from brynq_sdk_functions import Functions
|
|
5
|
+
|
|
6
|
+
class Companies:
|
|
7
|
+
"""
|
|
8
|
+
Handles all company-related operations in Zenegy API
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, zenegy):
|
|
12
|
+
"""
|
|
13
|
+
Initialize the Companies class.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
zenegy: The Zenegy instance to use for API calls
|
|
17
|
+
"""
|
|
18
|
+
self.zenegy = zenegy
|
|
19
|
+
self.endpoint = "api/companies"
|
|
20
|
+
|
|
21
|
+
def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
22
|
+
"""
|
|
23
|
+
GetCurrentUserCompanies
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple of (valid_data, invalid_data) DataFrames with company information
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
# Make the API request and get raw response
|
|
30
|
+
content = self.zenegy.get(endpoint=self.endpoint)
|
|
31
|
+
|
|
32
|
+
# Normalize the data (content is already an array)
|
|
33
|
+
df = pd.DataFrame(content)
|
|
34
|
+
|
|
35
|
+
if df.empty:
|
|
36
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
37
|
+
|
|
38
|
+
# Validate data using schema
|
|
39
|
+
valid_data, invalid_data = Functions.validate_data(df, CompaniesGet)
|
|
40
|
+
return valid_data, invalid_data
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
raise Exception(f"Failed to retrieve companies: {str(e)}") from e
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
from .schemas.company_cost_centers import (CostCenterCreate,
|
|
4
|
+
CostCentersGet)
|
|
5
|
+
from brynq_sdk_functions import Functions
|
|
6
|
+
from typing import Dict, Any, List, Tuple
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CostCenter:
|
|
11
|
+
"""
|
|
12
|
+
Handles all company cost center related operations in Zenegy API
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, zenegy):
|
|
16
|
+
"""
|
|
17
|
+
Initialize the Companycostcenters class.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
zenegy: The Zenegy instance to use for API calls
|
|
21
|
+
"""
|
|
22
|
+
self.zenegy = zenegy
|
|
23
|
+
self.endpoint = f"api/companies/{self.zenegy.company_uid}/cost-center"
|
|
24
|
+
|
|
25
|
+
def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
26
|
+
"""
|
|
27
|
+
GetCompanyCostCentersAsync
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple of (valid_data, invalid_data) DataFrames with cost center information
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
# Make the API request and get raw response
|
|
33
|
+
content = self.zenegy.get(endpoint=self.endpoint)
|
|
34
|
+
|
|
35
|
+
# Normalize the data (content is already an array)
|
|
36
|
+
df = pd.DataFrame(content)
|
|
37
|
+
|
|
38
|
+
if df.empty:
|
|
39
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
40
|
+
|
|
41
|
+
# Validate data using schema
|
|
42
|
+
valid_data, invalid_data = Functions.validate_data(df, CostCentersGet)
|
|
43
|
+
return valid_data, invalid_data
|
|
44
|
+
|
|
45
|
+
except Exception as e:
|
|
46
|
+
raise Exception(f"Failed to retrieve cost centers: {str(e)}") from e
|
|
47
|
+
|
|
48
|
+
def create(self, data: Dict[str, Any]) -> requests.Response:
|
|
49
|
+
"""
|
|
50
|
+
CreateCostCenterAsync
|
|
51
|
+
Args:
|
|
52
|
+
data (Dict[str, Any]): The data
|
|
53
|
+
Returns:
|
|
54
|
+
requests.Response: The API response
|
|
55
|
+
"""
|
|
56
|
+
# Validate the data using Pydantic
|
|
57
|
+
try:
|
|
58
|
+
req_data = CostCenterCreate(**data)
|
|
59
|
+
req_body = req_data.model_dump(by_alias=True, mode='json',exclude_none=True)
|
|
60
|
+
response = self.zenegy.post(endpoint=self.endpoint, json=req_body)
|
|
61
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
62
|
+
return response
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise Exception(f"Failed to create cost center: {str(e)}")
|
|
65
|
+
|
|
66
|
+
def delete(self, cost_center_uid: UUID) -> requests.Response:
|
|
67
|
+
"""
|
|
68
|
+
DeleteCostCenterAsync
|
|
69
|
+
Args:
|
|
70
|
+
cost_center_uid (UUID): The cost center uid
|
|
71
|
+
Returns:
|
|
72
|
+
requests.Response: The API response
|
|
73
|
+
"""
|
|
74
|
+
endpoint = f"{self.endpoint}/{cost_center_uid}"
|
|
75
|
+
try:
|
|
76
|
+
response = self.zenegy.delete(endpoint=endpoint)
|
|
77
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
78
|
+
return response
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise Exception(f"Failed to delete cost center: {str(e)}")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from uuid import UUID
|
|
2
|
+
from .schemas.company_departments import DepartmentsGet
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from brynq_sdk_functions import Functions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CompanyDepartments:
|
|
9
|
+
"""
|
|
10
|
+
Handles all companydepartment-related operations in Zenegy API
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, zenegy):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the CompanyDepartments class.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
zenegy: The Zenegy instance to use for API calls
|
|
19
|
+
"""
|
|
20
|
+
self.zenegy = zenegy
|
|
21
|
+
self.endpoint = f"api/companies/{self.zenegy.company_uid}/departments"
|
|
22
|
+
|
|
23
|
+
def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
24
|
+
"""
|
|
25
|
+
GetCompanyDepartmentsAsync
|
|
26
|
+
Returns:
|
|
27
|
+
Tuple of (valid_data, invalid_data) DataFrames with department information
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
# Make the API request and get raw response
|
|
31
|
+
content = self.zenegy.get(endpoint=self.endpoint)
|
|
32
|
+
|
|
33
|
+
# Get data from response
|
|
34
|
+
data = content.get("data", [])
|
|
35
|
+
|
|
36
|
+
df = pd.json_normalize(
|
|
37
|
+
data,
|
|
38
|
+
sep='__'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if df.empty:
|
|
42
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
43
|
+
|
|
44
|
+
# Validate data using schema
|
|
45
|
+
valid_data, invalid_data = Functions.validate_data(df, DepartmentsGet)
|
|
46
|
+
return valid_data, invalid_data
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
raise Exception(f"Failed to retrieve departments: {str(e)}") from e
|
|
50
|
+
|
|
51
|
+
def get_by_id(self, company_department_uid: UUID) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
52
|
+
"""
|
|
53
|
+
GetCompanyDepartment
|
|
54
|
+
Args:
|
|
55
|
+
company_department_uid (UUID): The company department uid
|
|
56
|
+
Returns:
|
|
57
|
+
Tuple of (valid_data, invalid_data) DataFrames with department information
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
endpoint = f"{self.endpoint}/{company_department_uid}"
|
|
61
|
+
# Make the API request and get raw response
|
|
62
|
+
content = self.zenegy.get(endpoint=endpoint)
|
|
63
|
+
|
|
64
|
+
df = pd.json_normalize(
|
|
65
|
+
content,
|
|
66
|
+
sep='__'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if df.empty:
|
|
70
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
71
|
+
|
|
72
|
+
# Validate data using schema
|
|
73
|
+
valid_data, invalid_data = Functions.validate_data(df, DepartmentsGet)
|
|
74
|
+
return valid_data, invalid_data
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise Exception(f"Failed to retrieve department by ID: {str(e)}") from e
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from uuid import UUID
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from brynq_sdk_functions import Functions
|
|
6
|
+
from .schemas.employee_documents import EmployeeDocumentsGet
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EmployeeDocuments:
|
|
10
|
+
"""Handles all employee document related operations in the Zenegy API"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, zenegy):
|
|
13
|
+
"""Initialize the EmployeeDocuments class with a Zenegy client instance."""
|
|
14
|
+
self.zenegy = zenegy
|
|
15
|
+
|
|
16
|
+
def get(self, employee_uid: UUID) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
17
|
+
"""
|
|
18
|
+
Retrieve all documents for a given employee.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
employee_uid: The employee UID.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Tuple of (valid_data, invalid_data) Pandas DataFrames.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
endpoint = (
|
|
28
|
+
f"api/employees/{employee_uid}/companies/{self.zenegy.company_uid}/documents"
|
|
29
|
+
)
|
|
30
|
+
content = self.zenegy.get(endpoint=endpoint)
|
|
31
|
+
|
|
32
|
+
records = content if isinstance(content, list) else content.get("data", [])
|
|
33
|
+
df = pd.json_normalize(records, sep="__")
|
|
34
|
+
if df.empty:
|
|
35
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
36
|
+
|
|
37
|
+
valid_data, invalid_data = Functions.validate_data(df, EmployeeDocumentsGet)
|
|
38
|
+
return valid_data, invalid_data
|
|
39
|
+
except Exception as e:
|
|
40
|
+
raise Exception(f"Failed to retrieve employee documents: {str(e)}") from e
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Generated endpoint class for tag: Employees
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import requests
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
from brynq_sdk_functions import Functions
|
|
6
|
+
|
|
7
|
+
from .schemas.employees import (EmployeeCreate,
|
|
8
|
+
EmployeeUpdate,
|
|
9
|
+
EmployeesGet,
|
|
10
|
+
EmployeesGetById,
|
|
11
|
+
EmployeeEmploymentDataUpdate,
|
|
12
|
+
EmployeeEmploymentUpdate,
|
|
13
|
+
EmployeeAdditionalUpdate,
|
|
14
|
+
StartSaldo,
|
|
15
|
+
EmployeePatch)
|
|
16
|
+
from .paychecks import PayChecks
|
|
17
|
+
from .pensions import Pensions
|
|
18
|
+
from typing import Dict, Any, Tuple, List
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Employees:
|
|
22
|
+
"""
|
|
23
|
+
Handles all employees-related operations in Zenegy API
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, zenegy):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the Employees class.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
zenegy: The Zenegy instance to use for API calls
|
|
32
|
+
"""
|
|
33
|
+
self.zenegy = zenegy
|
|
34
|
+
self.endpoint = f"api/companies/{self.zenegy.company_uid}/employees"
|
|
35
|
+
|
|
36
|
+
# Initialize paychecks and pensions
|
|
37
|
+
self.paychecks = PayChecks(zenegy)
|
|
38
|
+
self.pensions = Pensions(zenegy)
|
|
39
|
+
|
|
40
|
+
def get(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
41
|
+
"""
|
|
42
|
+
GetEmployeeBasesAsync
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple of (valid_data, invalid_data) DataFrames with employee information
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
# Make the API request and get raw response
|
|
48
|
+
content = self.zenegy.get(endpoint=self.endpoint)
|
|
49
|
+
|
|
50
|
+
# Get data from response
|
|
51
|
+
data = content.get("data", [])
|
|
52
|
+
|
|
53
|
+
# Normalize the data
|
|
54
|
+
df = pd.json_normalize(
|
|
55
|
+
data,
|
|
56
|
+
sep='__'
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if df.empty:
|
|
60
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
61
|
+
|
|
62
|
+
# Validate data using schema
|
|
63
|
+
valid_data, invalid_data = Functions.validate_data(df, EmployeesGet)
|
|
64
|
+
return valid_data, invalid_data
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise Exception(f"Failed to retrieve employees: {str(e)}") from e
|
|
68
|
+
|
|
69
|
+
def get_by_id(self, employee_uid: UUID) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
70
|
+
"""
|
|
71
|
+
GetEmployeeAsync
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
employee_uid (UUID): The employee uid
|
|
75
|
+
Returns:
|
|
76
|
+
Tuple of (valid_data, invalid_data) DataFrames with employee information
|
|
77
|
+
"""
|
|
78
|
+
endpoint = f"{self.endpoint}/{employee_uid}"
|
|
79
|
+
try:
|
|
80
|
+
# Make the API request and get raw response
|
|
81
|
+
content = self.zenegy.get(endpoint=endpoint)
|
|
82
|
+
|
|
83
|
+
# Normalize the data (content is already a dict)
|
|
84
|
+
df = pd.json_normalize(
|
|
85
|
+
[content], # Wrap single object in list for normalization
|
|
86
|
+
sep='__'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if df.empty:
|
|
90
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
91
|
+
|
|
92
|
+
# Validate data using detailed by-id schema
|
|
93
|
+
valid_data, invalid_data = Functions.validate_data(df, EmployeesGetById)
|
|
94
|
+
return valid_data, invalid_data
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise Exception(f"Failed to retrieve employee by ID: {str(e)}") from e
|
|
98
|
+
|
|
99
|
+
def create(self, data: Dict[str, Any]) -> requests.Response:
|
|
100
|
+
"""
|
|
101
|
+
PostEmployeeAsync
|
|
102
|
+
Args:
|
|
103
|
+
data (Dict[str, Any]): The data
|
|
104
|
+
Returns:
|
|
105
|
+
requests.Response: The API response
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
req_data = EmployeeCreate(**data)
|
|
109
|
+
req_body = req_data.model_dump(by_alias=True, mode='json', exclude_none=True)
|
|
110
|
+
response = self.zenegy.post(endpoint=self.endpoint.lstrip('/'), json=req_body)
|
|
111
|
+
patch_body = self.create_update_body(data)
|
|
112
|
+
|
|
113
|
+
# if the patch body is bigger than the request body, that means that there are fields left for the patch:
|
|
114
|
+
# Count actual fields at the lowest level for proper comparison
|
|
115
|
+
patch_field_count = self._count_nested_fields(patch_body)
|
|
116
|
+
if patch_field_count > len(req_body):
|
|
117
|
+
response_data = response.json()
|
|
118
|
+
if isinstance(response_data, str):
|
|
119
|
+
uid = response_data
|
|
120
|
+
elif isinstance(response_data, dict):
|
|
121
|
+
uid = response_data['data']['uid']
|
|
122
|
+
patch_endpoint = f"{self.endpoint}/{uid}"
|
|
123
|
+
|
|
124
|
+
response = self.zenegy.put(
|
|
125
|
+
endpoint=patch_endpoint.lstrip('/'),
|
|
126
|
+
json=patch_body
|
|
127
|
+
)
|
|
128
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
129
|
+
return response
|
|
130
|
+
|
|
131
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
132
|
+
return response
|
|
133
|
+
except Exception as e:
|
|
134
|
+
raise Exception(f"Failed to create employee: {str(e)}")
|
|
135
|
+
|
|
136
|
+
def upsert(self, data: Dict[str, Any]) -> requests.Response:
|
|
137
|
+
"""
|
|
138
|
+
UpsertEmployeeAsync
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
data (Dict[str, Any]): The data to update
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
requests.Response: The API response
|
|
145
|
+
"""
|
|
146
|
+
endpoint = f"api/companies/{self.zenegy.company_uid}/employees"
|
|
147
|
+
try:
|
|
148
|
+
req_body = self.create_update_body(data)
|
|
149
|
+
uid = data.get('employee_uid')
|
|
150
|
+
endpoint = f"{self.endpoint}/{uid}".lstrip('/')
|
|
151
|
+
response = self.zenegy.put(endpoint=endpoint, json=req_body)
|
|
152
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
153
|
+
return response
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise Exception(f"Failed to update employee: {str(e)}")
|
|
156
|
+
|
|
157
|
+
def delete(self, employee_uid: UUID) -> requests.Response:
|
|
158
|
+
"""
|
|
159
|
+
DeleteEmployee
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
employee_uid (UUID): The employee uid
|
|
163
|
+
Returns:
|
|
164
|
+
requests.Response: The API response
|
|
165
|
+
"""
|
|
166
|
+
endpoint = f"{self.endpoint}/{employee_uid}"
|
|
167
|
+
try:
|
|
168
|
+
response = self.zenegy.delete(endpoint=endpoint)
|
|
169
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
170
|
+
return response
|
|
171
|
+
except Exception as e:
|
|
172
|
+
raise Exception(f"Failed to delete employee: {str(e)}")
|
|
173
|
+
|
|
174
|
+
def create_update_body(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
175
|
+
"""
|
|
176
|
+
Create the update body for the employee.
|
|
177
|
+
"""
|
|
178
|
+
# There is strange logic in Zenegy that you can create an employee with only a limited set of fields. Afterward, you have to patch the employee with the rest of the fields.
|
|
179
|
+
update_data = EmployeeUpdate(**data)
|
|
180
|
+
body = update_data.model_dump(by_alias=True, mode='json', exclude_none=True)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
#-- START TODO: This is a temporary solution to handle the additional fields for the contract related fields in mft.
|
|
184
|
+
#additional fields for contract related fields
|
|
185
|
+
schema_map = {
|
|
186
|
+
"startSaldo": StartSaldo,
|
|
187
|
+
"employmentData": EmployeeEmploymentDataUpdate,
|
|
188
|
+
"employeeEmployment": EmployeeEmploymentUpdate,
|
|
189
|
+
"employeeAditional": EmployeeAdditionalUpdate
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
result_body = {"updateEmployeeBase": body}
|
|
193
|
+
|
|
194
|
+
# Initialize required top-level fields with empty objects (API requires these to always be present)
|
|
195
|
+
result_body["startSaldo"] = {} # can be passed empty
|
|
196
|
+
result_body["employeeEmployment"] = {}
|
|
197
|
+
result_body["employeeAditional"] = {"monthlySalaryFixedBase": 0}
|
|
198
|
+
|
|
199
|
+
# Check if any fields from data match schema fields, and serialize accordingly
|
|
200
|
+
for schema_name, schema_class in schema_map.items():
|
|
201
|
+
schema_fields = schema_class.model_fields.keys()
|
|
202
|
+
# Find matching keys between data and schema fields
|
|
203
|
+
matching_data = {k: v for k, v in data.items() if k in schema_fields}
|
|
204
|
+
|
|
205
|
+
# Skip if employee_number is the only field present
|
|
206
|
+
if matching_data: # and not (len(matching_data) == 1 and 'employee_number' in matching_data):
|
|
207
|
+
# Serialize the matching data using the schema
|
|
208
|
+
schema_instance = schema_class(**matching_data)
|
|
209
|
+
serialized_data = schema_instance.model_dump(by_alias=True, mode='json', exclude_none=True)
|
|
210
|
+
|
|
211
|
+
# startSaldo appears both inside updateEmployeeBase and at top level
|
|
212
|
+
if schema_name == "startSaldo":
|
|
213
|
+
result_body["updateEmployeeBase"][schema_name] = serialized_data
|
|
214
|
+
result_body[schema_name] = serialized_data
|
|
215
|
+
# employmentData appears inside updateEmployeeBase
|
|
216
|
+
elif schema_name == "employmentData":
|
|
217
|
+
result_body["updateEmployeeBase"][schema_name] = serialized_data
|
|
218
|
+
# employeeEmployment and employeeAditional appear at top level
|
|
219
|
+
else:
|
|
220
|
+
result_body[schema_name] = serialized_data
|
|
221
|
+
#--END TODO
|
|
222
|
+
return result_body
|
|
223
|
+
|
|
224
|
+
def _count_nested_fields(self, data: Dict[str, Any]) -> int:
|
|
225
|
+
"""
|
|
226
|
+
Count the actual fields at the lowest level of a nested dictionary.
|
|
227
|
+
https://www.google.com/search?client=firefox-b-d&sca_esv=4acce884baa46368&sxsrf=AE3TifPbAKp8zW8Gczemzqd3WYBgKrcwlg:1761129017606&q=recursion&spell=1&sa=X&ved=2ahUKEwja3v3rzLeQAxUwwAIHHZJVKZUQBSgAegQIFhAB
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
data: Dictionary that may contain nested dictionaries
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
int: Total count of fields at the lowest level
|
|
234
|
+
"""
|
|
235
|
+
count = 0
|
|
236
|
+
for value in data.values():
|
|
237
|
+
if isinstance(value, dict):
|
|
238
|
+
count += self._count_nested_fields(value)
|
|
239
|
+
else:
|
|
240
|
+
count += 1
|
|
241
|
+
return count
|
|
242
|
+
|
|
243
|
+
def patch(self, employee_uid: UUID, data: Dict[str, Any], op: str = "replace") -> requests.Response:
|
|
244
|
+
"""
|
|
245
|
+
PatchEmployee
|
|
246
|
+
|
|
247
|
+
Single entry point for patching employees using a flat data dictionary.
|
|
248
|
+
Flat keys may include prefixes for nested fields using a single underscore '_',
|
|
249
|
+
e.g., 'start_saldo_start_g_days', 'language_name'. All generated operations
|
|
250
|
+
are sent in ONE PATCH request as a JSON array per endpoint capability.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
employee_uid (UUID): The employee uid
|
|
254
|
+
data (Dict[str, Any]): Flat dictionary with EmployeeUpdate fields (supports '_' prefix nesting in keys)
|
|
255
|
+
op (str): Operation type; defaults to "replace"
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
requests.Response: Single response from the batch PATCH request
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
if not op:
|
|
262
|
+
raise ValueError("Patch operation 'op' must be provided")
|
|
263
|
+
|
|
264
|
+
operations = self.build_patch_operations(data=data, op=op)
|
|
265
|
+
endpoint = f"{self.endpoint}/{employee_uid}".lstrip('/')
|
|
266
|
+
response = self.zenegy.patch(endpoint=endpoint, json=operations)
|
|
267
|
+
self.zenegy.raise_for_status_with_details(response)
|
|
268
|
+
return response
|
|
269
|
+
except Exception as e:
|
|
270
|
+
raise Exception(f"Failed to patch employee: {str(e)}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def build_patch_operations(self, data: Dict[str, Any], op: str = "replace") -> List[Dict[str, Any]]:
|
|
274
|
+
"""
|
|
275
|
+
Build JSON Patch operations from a flat employee data dictionary using
|
|
276
|
+
the EmployeePatch flat schema (aliases map directly to JSON Patch paths).
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
data (Dict[str, Any]): Flat data with pythonic keys (e.g., name, email, start_g_days)
|
|
280
|
+
op (str): JSON Patch op. Defaults to "replace".
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
List[Dict[str, Any]]: JSON Patch operations
|
|
284
|
+
"""
|
|
285
|
+
if not isinstance(data, dict):
|
|
286
|
+
raise ValueError("data must be a dictionary")
|
|
287
|
+
if not op:
|
|
288
|
+
raise ValueError("Patch operation 'op' must be provided")
|
|
289
|
+
|
|
290
|
+
# Validate against flat schema and dump using alias names
|
|
291
|
+
validated = EmployeePatch(**data)
|
|
292
|
+
alias_dump: Dict[str, Any] = validated.model_dump(by_alias=True, mode='json', exclude_none=True, exclude_unset=True)
|
|
293
|
+
|
|
294
|
+
operations: List[Dict[str, Any]] = []
|
|
295
|
+
for alias_key, value in alias_dump.items():
|
|
296
|
+
operations.append({
|
|
297
|
+
"op": op,
|
|
298
|
+
"path": f"/{alias_key}",
|
|
299
|
+
"value": value,
|
|
300
|
+
})
|
|
301
|
+
return operations
|