brynq-sdk-sage-germany 1.0.0__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_sage_germany/__init__.py +278 -0
- brynq_sdk_sage_germany/absences.py +175 -0
- brynq_sdk_sage_germany/allowances.py +100 -0
- brynq_sdk_sage_germany/contracts.py +145 -0
- brynq_sdk_sage_germany/cost_centers.py +89 -0
- brynq_sdk_sage_germany/employees.py +140 -0
- brynq_sdk_sage_germany/helpers.py +391 -0
- brynq_sdk_sage_germany/organization.py +90 -0
- brynq_sdk_sage_germany/payroll.py +167 -0
- brynq_sdk_sage_germany/payslips.py +106 -0
- brynq_sdk_sage_germany/salaries.py +95 -0
- brynq_sdk_sage_germany/schemas/__init__.py +44 -0
- brynq_sdk_sage_germany/schemas/absences.py +311 -0
- brynq_sdk_sage_germany/schemas/allowances.py +147 -0
- brynq_sdk_sage_germany/schemas/cost_centers.py +46 -0
- brynq_sdk_sage_germany/schemas/employees.py +487 -0
- brynq_sdk_sage_germany/schemas/organization.py +172 -0
- brynq_sdk_sage_germany/schemas/organization_assignment.py +61 -0
- brynq_sdk_sage_germany/schemas/payroll.py +287 -0
- brynq_sdk_sage_germany/schemas/payslips.py +34 -0
- brynq_sdk_sage_germany/schemas/salaries.py +101 -0
- brynq_sdk_sage_germany/schemas/start_end_dates.py +194 -0
- brynq_sdk_sage_germany/schemas/vacation_account.py +117 -0
- brynq_sdk_sage_germany/schemas/work_hours.py +94 -0
- brynq_sdk_sage_germany/start_end_dates.py +123 -0
- brynq_sdk_sage_germany/vacation_account.py +70 -0
- brynq_sdk_sage_germany/work_hours.py +97 -0
- brynq_sdk_sage_germany-1.0.0.dist-info/METADATA +21 -0
- brynq_sdk_sage_germany-1.0.0.dist-info/RECORD +31 -0
- brynq_sdk_sage_germany-1.0.0.dist-info/WHEEL +5 -0
- brynq_sdk_sage_germany-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Organization endpoint implementations for Sage Germany.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from brynq_sdk_functions import Functions
|
|
11
|
+
|
|
12
|
+
from .helpers import organization_flat_to_nested
|
|
13
|
+
from .schemas.organization import OrganizationCreate, OrganizationGet
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Organization:
|
|
17
|
+
"""
|
|
18
|
+
Handles organization structure and cost allocation operations scoped to employees.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, sage) -> None:
|
|
22
|
+
self.sage = sage
|
|
23
|
+
self.base_url = "/employeenew/person/Organisation"
|
|
24
|
+
|
|
25
|
+
def _collect_employee_keys(self) -> List[Dict[str, int]]:
|
|
26
|
+
"""
|
|
27
|
+
Retrieve distinct MdNr/AnNr pairs using the employee search endpoint.
|
|
28
|
+
"""
|
|
29
|
+
return self.sage._employee_search()
|
|
30
|
+
|
|
31
|
+
def get(
|
|
32
|
+
self,
|
|
33
|
+
date: Optional[str] = None,
|
|
34
|
+
company_id: Optional[int] = None,
|
|
35
|
+
employee_number: Optional[int] = None,
|
|
36
|
+
) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
37
|
+
"""
|
|
38
|
+
Retrieve organization data (cost centers, cost units, org structures) for employees.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
if company_id is not None and employee_number is not None:
|
|
42
|
+
employee_keys = [{"MdNr": company_id, "AnNr": employee_number}]
|
|
43
|
+
else:
|
|
44
|
+
employee_keys = self._collect_employee_keys()
|
|
45
|
+
|
|
46
|
+
if not employee_keys:
|
|
47
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
48
|
+
|
|
49
|
+
organization_records: List[Dict[str, Any]] = []
|
|
50
|
+
effective_date = date or datetime.now(timezone.utc).strftime("%Y-%m-%dT00:00:00")
|
|
51
|
+
|
|
52
|
+
for key in employee_keys:
|
|
53
|
+
response = self.sage.get(
|
|
54
|
+
path=self.base_url,
|
|
55
|
+
params={"MdNr": key["MdNr"], "AnNr": key["AnNr"], "Date": effective_date},
|
|
56
|
+
)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
payload = response.json()
|
|
59
|
+
if isinstance(payload, dict):
|
|
60
|
+
organization_records.append(payload)
|
|
61
|
+
|
|
62
|
+
if not organization_records:
|
|
63
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
64
|
+
|
|
65
|
+
dataframe = pd.json_normalize(organization_records, sep="__")
|
|
66
|
+
|
|
67
|
+
valid_data, invalid_data = Functions.validate_data(
|
|
68
|
+
df=dataframe,
|
|
69
|
+
schema=OrganizationGet,
|
|
70
|
+
)
|
|
71
|
+
return valid_data, invalid_data
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
raise RuntimeError("Failed to retrieve organization data.") from exc
|
|
74
|
+
|
|
75
|
+
def create(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Create or update organization allocation data for an employee.
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
nested_payload = organization_flat_to_nested(data)
|
|
81
|
+
payload = OrganizationCreate(**nested_payload).model_dump(by_alias=True, exclude_none=True, mode="json")
|
|
82
|
+
|
|
83
|
+
response = self.sage.post(
|
|
84
|
+
path=self.base_url,
|
|
85
|
+
body=payload,
|
|
86
|
+
)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
return response
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
raise RuntimeError("Failed to create Sage Germany organization allocation.") from exc
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Payroll endpoint implementations for Sage Germany.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from brynq_sdk_functions import Functions
|
|
12
|
+
|
|
13
|
+
from .helpers import sage_flat_to_nested_with_prefix
|
|
14
|
+
from .schemas import PayrollGet, PayrollMasterDataCreate, PayrollMasterDataGet
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .employees import Employees
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Payroll:
|
|
21
|
+
"""
|
|
22
|
+
Handles payroll-related operations scoped to employees.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, sage) -> None:
|
|
26
|
+
self.sage = sage
|
|
27
|
+
self.master_data_url = "/employeenew/abrechnungsdaten/grunddaten"
|
|
28
|
+
self.master_data_create_url = "/employeenew/abrechnungsdaten/grunddaten"
|
|
29
|
+
self.payroll_url = "/pay/abrechnung/abrechnungsdaten"
|
|
30
|
+
|
|
31
|
+
def get_master_data(
|
|
32
|
+
self,
|
|
33
|
+
date: Optional[str] = None,
|
|
34
|
+
company_id: Optional[int] = None,
|
|
35
|
+
employee_number: Optional[int] = None,
|
|
36
|
+
) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
37
|
+
"""
|
|
38
|
+
Retrieve payroll master data for a specific employee or all employees.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
if company_id is not None and employee_number is not None:
|
|
42
|
+
employee_keys = [{"MdNr": company_id, "AnNr": employee_number}]
|
|
43
|
+
else:
|
|
44
|
+
employee_keys = self.sage._employee_search()
|
|
45
|
+
|
|
46
|
+
if not employee_keys:
|
|
47
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
48
|
+
|
|
49
|
+
master_records: List[Dict[str, Any]] = []
|
|
50
|
+
effective_date = date or datetime.now(timezone.utc).strftime("%Y-%m-%dT00:00:00")
|
|
51
|
+
|
|
52
|
+
for key in employee_keys:
|
|
53
|
+
response = self.sage.get(
|
|
54
|
+
path=self.master_data_url,
|
|
55
|
+
params={"MdNr": key["MdNr"], "AnNr": key["AnNr"], "date": effective_date},
|
|
56
|
+
)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
payload = response.json()
|
|
59
|
+
if isinstance(payload, dict):
|
|
60
|
+
master_records.append(payload)
|
|
61
|
+
|
|
62
|
+
if not master_records:
|
|
63
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
64
|
+
|
|
65
|
+
dataframe = pd.json_normalize(master_records, sep="__")
|
|
66
|
+
|
|
67
|
+
valid_data, invalid_data = Functions.validate_data(
|
|
68
|
+
df=dataframe,
|
|
69
|
+
schema=PayrollMasterDataGet,
|
|
70
|
+
)
|
|
71
|
+
return valid_data, invalid_data
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
raise RuntimeError("Failed to retrieve payroll master data.") from exc
|
|
74
|
+
|
|
75
|
+
def get(
|
|
76
|
+
self,
|
|
77
|
+
date: Optional[str] = None,
|
|
78
|
+
company_id: Optional[int] = None,
|
|
79
|
+
employee_number: Optional[int] = None,
|
|
80
|
+
) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
81
|
+
"""
|
|
82
|
+
Retrieve payroll run results (Brutto/Netto, tax, and insurance breakdowns).
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
if company_id is not None and employee_number is not None:
|
|
86
|
+
employee_keys = [{"MdNr": company_id, "AnNr": employee_number}]
|
|
87
|
+
else:
|
|
88
|
+
employee_keys = self.sage._employee_search()
|
|
89
|
+
|
|
90
|
+
if not employee_keys:
|
|
91
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
92
|
+
|
|
93
|
+
payroll_records: List[Dict[str, Any]] = []
|
|
94
|
+
effective_date = date or datetime.now(timezone.utc).strftime("%Y-%m-%dT00:00:00")
|
|
95
|
+
|
|
96
|
+
for key in employee_keys:
|
|
97
|
+
response = self.sage.get(
|
|
98
|
+
path=self.payroll_url,
|
|
99
|
+
params={"MdNr": key["MdNr"], "AnNr": key["AnNr"], "date": effective_date},
|
|
100
|
+
)
|
|
101
|
+
response.raise_for_status()
|
|
102
|
+
payload = response.json()
|
|
103
|
+
|
|
104
|
+
if isinstance(payload, dict):
|
|
105
|
+
payload_items = [payload]
|
|
106
|
+
elif isinstance(payload, list):
|
|
107
|
+
payload_items = payload
|
|
108
|
+
else:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
for entry in payload_items:
|
|
112
|
+
if not isinstance(entry, dict):
|
|
113
|
+
continue
|
|
114
|
+
enriched_entry = entry.copy()
|
|
115
|
+
enriched_entry["company_id"] = key["MdNr"]
|
|
116
|
+
enriched_entry["employee_number"] = enriched_entry.get("Personalnummer", key["AnNr"])
|
|
117
|
+
enriched_entry["source_date"] = effective_date
|
|
118
|
+
payroll_records.append(enriched_entry)
|
|
119
|
+
|
|
120
|
+
if not payroll_records:
|
|
121
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
122
|
+
|
|
123
|
+
dataframe = pd.json_normalize(payroll_records, sep="__").copy()
|
|
124
|
+
|
|
125
|
+
dataframe["company_id"] = (
|
|
126
|
+
pd.to_numeric(dataframe["company_id"], errors="coerce").astype("Int64")
|
|
127
|
+
)
|
|
128
|
+
dataframe["employee_number"] = (
|
|
129
|
+
pd.to_numeric(dataframe["employee_number"], errors="coerce").astype("Int64")
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
source_dates = pd.to_datetime(
|
|
133
|
+
dataframe["source_date"], errors="coerce"
|
|
134
|
+
).dt.strftime("%Y-%m-%d")
|
|
135
|
+
|
|
136
|
+
dataframe["combined_key"] = (
|
|
137
|
+
dataframe["company_id"].astype("string")
|
|
138
|
+
+ "_"
|
|
139
|
+
+ dataframe["employee_number"].astype("string")
|
|
140
|
+
+ "_"
|
|
141
|
+
+ source_dates.fillna("")
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
valid_data, invalid_data = Functions.validate_data(
|
|
145
|
+
df=dataframe,
|
|
146
|
+
schema=PayrollGet,
|
|
147
|
+
)
|
|
148
|
+
return valid_data, invalid_data
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
raise RuntimeError("Failed to retrieve payroll data.") from exc
|
|
151
|
+
|
|
152
|
+
def create_master_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Create or update payroll master data (abrechnungsdaten/grunddaten).
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
nested_payload = sage_flat_to_nested_with_prefix(data, PayrollMasterDataCreate)
|
|
158
|
+
payload = PayrollMasterDataCreate(**nested_payload).model_dump(by_alias=True, exclude_none=True, mode="json")
|
|
159
|
+
|
|
160
|
+
response = self.sage.post(
|
|
161
|
+
path=self.master_data_create_url,
|
|
162
|
+
body=payload,
|
|
163
|
+
)
|
|
164
|
+
response.raise_for_status()
|
|
165
|
+
return response
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
raise RuntimeError("Failed to create Sage Germany payroll master data.") from exc
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Payroll document endpoints (payslip metadata + download) for Sage Germany.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from brynq_sdk_functions import Functions
|
|
11
|
+
|
|
12
|
+
from .schemas import PayslipsGet
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Payslips:
|
|
18
|
+
"""
|
|
19
|
+
Retrieves and downloads payslip-style payroll documents (Verdienstbescheinigung §108 GewO).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, sage) -> None:
|
|
23
|
+
self.sage = sage
|
|
24
|
+
self.base_url = "/Employee/Pay/Documents"
|
|
25
|
+
|
|
26
|
+
def _fetch_payload(self, request_params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
27
|
+
response = self.sage.get(
|
|
28
|
+
path=self.base_url,
|
|
29
|
+
params=request_params,
|
|
30
|
+
)
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
payload = response.json()
|
|
33
|
+
return payload if isinstance(payload, list) else []
|
|
34
|
+
|
|
35
|
+
def _fetch_all_employee_payslips(self) -> List[Dict[str, Any]]:
|
|
36
|
+
employee_keys = self.sage._employee_search() # noqa: SLF001
|
|
37
|
+
if not employee_keys:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
records: List[Dict[str, Any]] = []
|
|
41
|
+
for key in employee_keys:
|
|
42
|
+
key_params: Dict[str, Any] = {
|
|
43
|
+
"MdNr": key["MdNr"],
|
|
44
|
+
"AnNr": key["AnNr"],
|
|
45
|
+
"sbc": "false",
|
|
46
|
+
}
|
|
47
|
+
records.extend(self._fetch_payload(key_params))
|
|
48
|
+
return records
|
|
49
|
+
|
|
50
|
+
def get(
|
|
51
|
+
self,
|
|
52
|
+
period: Optional[str] = None,
|
|
53
|
+
company_id: Optional[int] = None,
|
|
54
|
+
employee_number: Optional[int] = None,
|
|
55
|
+
date: Optional[str] = None,
|
|
56
|
+
employee_shipping_type: Optional[int] = None,
|
|
57
|
+
sbc: Optional[bool] = False,
|
|
58
|
+
) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
59
|
+
"""
|
|
60
|
+
List payslips for the provided filters.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
params: Dict[str, Any] = {}
|
|
64
|
+
if period:
|
|
65
|
+
params["periode"] = period
|
|
66
|
+
if company_id is not None:
|
|
67
|
+
params["MdNr"] = company_id
|
|
68
|
+
if employee_number is not None:
|
|
69
|
+
params["AnNr"] = employee_number
|
|
70
|
+
if date:
|
|
71
|
+
params["Date"] = date
|
|
72
|
+
if employee_shipping_type is not None:
|
|
73
|
+
params["employeeshippingtype"] = employee_shipping_type
|
|
74
|
+
|
|
75
|
+
params["sbc"] = str(sbc).lower()
|
|
76
|
+
|
|
77
|
+
if params:
|
|
78
|
+
records = self._fetch_payload(params)
|
|
79
|
+
else:
|
|
80
|
+
records = self._fetch_all_employee_payslips()
|
|
81
|
+
|
|
82
|
+
if not records:
|
|
83
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
84
|
+
|
|
85
|
+
df = pd.json_normalize(records, sep="__")
|
|
86
|
+
valid_data, invalid_data = Functions.validate_data(
|
|
87
|
+
df=df,
|
|
88
|
+
schema=PayslipsGet,
|
|
89
|
+
)
|
|
90
|
+
return valid_data, invalid_data
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
raise RuntimeError("Failed to retrieve payslips.") from exc
|
|
93
|
+
|
|
94
|
+
def download(self, document_id: int, inline: bool = False) -> bytes:
|
|
95
|
+
"""
|
|
96
|
+
Download a single payslip (PDF binary).
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
response = self.sage.get(
|
|
100
|
+
path=self.base_url,
|
|
101
|
+
params={"Id": document_id, "Inline": str(inline).lower()},
|
|
102
|
+
)
|
|
103
|
+
response.raise_for_status()
|
|
104
|
+
return response.content
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
raise RuntimeError(f"Failed to download payslip {document_id}.") from exc
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Salary endpoint implementations for Sage Germany.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from brynq_sdk_functions import Functions
|
|
9
|
+
|
|
10
|
+
from .schemas.salaries import SalaryGet, SalaryImport
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Salary:
|
|
14
|
+
"""
|
|
15
|
+
Handles salary-related operations scoped to employees.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, sage) -> None:
|
|
19
|
+
self.sage = sage
|
|
20
|
+
self.base_url = "/employeenew/entgelte/lohngehalt"
|
|
21
|
+
|
|
22
|
+
def get(
|
|
23
|
+
self,
|
|
24
|
+
company_id: Optional[int] = None,
|
|
25
|
+
employee_number: Optional[int] = None,
|
|
26
|
+
) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
27
|
+
"""
|
|
28
|
+
Retrieve salary records for a single employee or all employees.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
if company_id is not None and employee_number is not None:
|
|
32
|
+
employee_keys = [{"MdNr": company_id, "AnNr": employee_number}]
|
|
33
|
+
else:
|
|
34
|
+
employee_keys = self.sage._employee_search()
|
|
35
|
+
|
|
36
|
+
if not employee_keys:
|
|
37
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
38
|
+
|
|
39
|
+
salary_records: List[Dict[str, Any]] = []
|
|
40
|
+
for key in employee_keys:
|
|
41
|
+
response = self.sage.get(
|
|
42
|
+
path=self.base_url,
|
|
43
|
+
params={"MdNr": key["MdNr"], "AnNr": key["AnNr"]},
|
|
44
|
+
)
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
payload = response.json()
|
|
47
|
+
if isinstance(payload, dict):
|
|
48
|
+
salary_records.append(payload)
|
|
49
|
+
|
|
50
|
+
if not salary_records:
|
|
51
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
52
|
+
|
|
53
|
+
df = pd.json_normalize(salary_records, sep="__")
|
|
54
|
+
|
|
55
|
+
valid_data, invalid_data = Functions.validate_data(
|
|
56
|
+
df=df,
|
|
57
|
+
schema=SalaryGet,
|
|
58
|
+
)
|
|
59
|
+
return valid_data, invalid_data
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
raise RuntimeError("Failed to retrieve salary data.") from exc
|
|
62
|
+
|
|
63
|
+
def update(self, data: Union[Dict[str, Any], List[Dict[str, Any]]]) -> Dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
Import or update gross salary (Bruttolohn) records.
|
|
66
|
+
|
|
67
|
+
Accepts a single dict or a list of dicts with Sage field names such as
|
|
68
|
+
Mdnr, Annr, AbrJahr, AbrMon, Tag, Lanr, Anzahl, Betrag, KoSt1, KoTr1, AA1, Zuschlag.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
if isinstance(data, dict):
|
|
72
|
+
items = [data]
|
|
73
|
+
elif isinstance(data, list):
|
|
74
|
+
if not data:
|
|
75
|
+
raise ValueError("Salary update payload list must not be empty.")
|
|
76
|
+
if not all(isinstance(item, dict) for item in data):
|
|
77
|
+
raise ValueError("Salary update payload list must contain only dictionaries.")
|
|
78
|
+
items = data
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError("Salary update payload must be a dictionary or list of dictionaries.")
|
|
81
|
+
|
|
82
|
+
# API requires a list payload even for single salary import items.
|
|
83
|
+
payload = [
|
|
84
|
+
SalaryImport(**item).model_dump(by_alias=True, exclude_none=True, mode="json")
|
|
85
|
+
for item in items
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
response = self.sage.post(
|
|
89
|
+
path="/Pay/Import/Bruttolohn",
|
|
90
|
+
body=payload,
|
|
91
|
+
)
|
|
92
|
+
response.raise_for_status()
|
|
93
|
+
return response
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
raise RuntimeError("Failed to update Sage Germany salary records.") from exc
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schema namespace for Sage Germany endpoints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .employees import EmployeeUpdateRequest, EmployeeCreateForm, EmployeesGet
|
|
6
|
+
from .absences import AbsenceTypesGet, AbsencesGet, AbsenceCreate, AbsenceDelete
|
|
7
|
+
|
|
8
|
+
from .salaries import SalaryGet, SalaryImport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .allowances import AllowancesGet, AllowanceCreateRequest
|
|
12
|
+
from .start_end_dates import StartEndDatesGet
|
|
13
|
+
from .work_hours import WorkHoursGet, WorkHoursCreate
|
|
14
|
+
from .payroll import PayrollGet, PayrollMasterDataGet, PayrollMasterDataCreate
|
|
15
|
+
from .payslips import PayslipsGet
|
|
16
|
+
from .vacation_account import VacationAccountGet
|
|
17
|
+
from .cost_centers import CostCentersGet, CostCenterCreate
|
|
18
|
+
from .organization import OrganizationGet, OrganizationCreate
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"EmployeesGet",
|
|
22
|
+
"EmployeeUpdateRequest",
|
|
23
|
+
"EmployeeCreateForm",
|
|
24
|
+
"AbsencesGet",
|
|
25
|
+
"AbsenceTypesGet",
|
|
26
|
+
"AbsenceCreate",
|
|
27
|
+
"AbsenceDelete",
|
|
28
|
+
"SalaryGet",
|
|
29
|
+
"SalaryImport",
|
|
30
|
+
"AllowancesGet",
|
|
31
|
+
"AllowanceCreateRequest",
|
|
32
|
+
"StartEndDatesGet",
|
|
33
|
+
"WorkHoursGet",
|
|
34
|
+
"WorkHoursCreate",
|
|
35
|
+
"PayrollGet",
|
|
36
|
+
"PayrollMasterDataGet",
|
|
37
|
+
"PayrollMasterDataCreate",
|
|
38
|
+
"PayslipsGet",
|
|
39
|
+
"VacationAccountGet",
|
|
40
|
+
"CostCentersGet",
|
|
41
|
+
"CostCenterCreate",
|
|
42
|
+
"OrganizationGet",
|
|
43
|
+
"OrganizationCreate",
|
|
44
|
+
]
|