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.
Files changed (31) hide show
  1. brynq_sdk_sage_germany/__init__.py +278 -0
  2. brynq_sdk_sage_germany/absences.py +175 -0
  3. brynq_sdk_sage_germany/allowances.py +100 -0
  4. brynq_sdk_sage_germany/contracts.py +145 -0
  5. brynq_sdk_sage_germany/cost_centers.py +89 -0
  6. brynq_sdk_sage_germany/employees.py +140 -0
  7. brynq_sdk_sage_germany/helpers.py +391 -0
  8. brynq_sdk_sage_germany/organization.py +90 -0
  9. brynq_sdk_sage_germany/payroll.py +167 -0
  10. brynq_sdk_sage_germany/payslips.py +106 -0
  11. brynq_sdk_sage_germany/salaries.py +95 -0
  12. brynq_sdk_sage_germany/schemas/__init__.py +44 -0
  13. brynq_sdk_sage_germany/schemas/absences.py +311 -0
  14. brynq_sdk_sage_germany/schemas/allowances.py +147 -0
  15. brynq_sdk_sage_germany/schemas/cost_centers.py +46 -0
  16. brynq_sdk_sage_germany/schemas/employees.py +487 -0
  17. brynq_sdk_sage_germany/schemas/organization.py +172 -0
  18. brynq_sdk_sage_germany/schemas/organization_assignment.py +61 -0
  19. brynq_sdk_sage_germany/schemas/payroll.py +287 -0
  20. brynq_sdk_sage_germany/schemas/payslips.py +34 -0
  21. brynq_sdk_sage_germany/schemas/salaries.py +101 -0
  22. brynq_sdk_sage_germany/schemas/start_end_dates.py +194 -0
  23. brynq_sdk_sage_germany/schemas/vacation_account.py +117 -0
  24. brynq_sdk_sage_germany/schemas/work_hours.py +94 -0
  25. brynq_sdk_sage_germany/start_end_dates.py +123 -0
  26. brynq_sdk_sage_germany/vacation_account.py +70 -0
  27. brynq_sdk_sage_germany/work_hours.py +97 -0
  28. brynq_sdk_sage_germany-1.0.0.dist-info/METADATA +21 -0
  29. brynq_sdk_sage_germany-1.0.0.dist-info/RECORD +31 -0
  30. brynq_sdk_sage_germany-1.0.0.dist-info/WHEEL +5 -0
  31. brynq_sdk_sage_germany-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,101 @@
1
+ """
2
+ Schemas for Sage Germany salary payloads.
3
+ """
4
+
5
+ from typing import Optional, Union
6
+
7
+ import pandas as pd
8
+ import pandera as pa
9
+ from pandera.typing import Series
10
+ from pydantic import BaseModel, Field, ConfigDict
11
+
12
+ from brynq_sdk_functions import BrynQPanderaDataFrameModel
13
+
14
+
15
+ class SalaryGet(BrynQPanderaDataFrameModel):
16
+ """
17
+ Pandera schema for employee salary records.
18
+ """
19
+
20
+ # Base identifiers
21
+ key_date: Series[pd.StringDtype] = pa.Field(alias="Key__Date", coerce=True, nullable=False, description="Snapshot timestamp.")
22
+ company_id: Series[pd.Int64Dtype] = pa.Field(alias="Key__MdNr", coerce=True, nullable=False, description="Company id.")
23
+ employee_number: Series[pd.Int64Dtype] = pa.Field(alias="Key__AnNr", coerce=True, nullable=False, description="Employee number.")
24
+ combined_key: Series[pd.StringDtype] = pa.Field(alias="Key__CombinedKey", coerce=True, nullable=False, description="Combined tenant/employee key.")
25
+ key_is_empty: Optional[Series[bool]] = pa.Field(alias="Key__IsEmpty", coerce=True, nullable=True, description="Indicates empty key payload.")
26
+
27
+ # Salary configuration flags
28
+ transfer_fixed_allowances: Optional[Series[bool]] = pa.Field(alias="FesteBezuegeUebernehmen", coerce=True, nullable=True, description="Transfer fixed allowances flag.")
29
+ collective_bargain_table: Optional[Series[bool]] = pa.Field(alias="Tariftabelle", coerce=True, nullable=True, description="Collective bargain table flag.")
30
+
31
+ # Agreed salary
32
+ agreed_salary_amount: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbartesEntgelt__Gehalt", coerce=True, nullable=True, description="Agreed salary amount.")
33
+ allowance_one_is_percentage: Optional[Series[bool]] = pa.Field(alias="VereinbartesEntgelt__Zulagen__Zulage1Prozentual", coerce=True, nullable=True, description="Allowance 1 percentage flag.")
34
+ allowance_two_is_percentage: Optional[Series[bool]] = pa.Field(alias="VereinbartesEntgelt__Zulagen__Zulage2Prozentual", coerce=True, nullable=True, description="Allowance 2 percentage flag.")
35
+ allowance_three_is_percentage: Optional[Series[bool]] = pa.Field(alias="VereinbartesEntgelt__Zulagen__Zulage3Prozentual", coerce=True, nullable=True, description="Allowance 3 percentage flag.")
36
+ allowance_one_amount: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbartesEntgelt__Zulagen__Zulage1Betrag", coerce=True, nullable=True, description="Allowance 1 amount.")
37
+ allowance_two_amount: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbartesEntgelt__Zulagen__Zulage2Betrag", coerce=True, nullable=True, description="Allowance 2 amount.")
38
+ allowance_three_amount: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbartesEntgelt__Zulagen__Zulage3Betrag", coerce=True, nullable=True, description="Allowance 3 amount.")
39
+ allowance_one_percentage: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbartesEntgelt__Zulagen__Zulage1Prozent", coerce=True, nullable=True, description="Allowance 1 percentage.")
40
+ allowance_two_percentage: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbartesEntgelt__Zulagen__Zulage2Prozent", coerce=True, nullable=True, description="Allowance 2 percentage.")
41
+ allowance_three_percentage: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbartesEntgelt__Zulagen__Zulage3Prozent", coerce=True, nullable=True, description="Allowance 3 percentage.")
42
+
43
+ # Hourly rates
44
+ hourly_rate_1: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz1", coerce=True, nullable=True, description="Hourly rate 1.")
45
+ hourly_rate_2: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz2", coerce=True, nullable=True, description="Hourly rate 2.")
46
+ hourly_rate_3: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz3", coerce=True, nullable=True, description="Hourly rate 3.")
47
+ hourly_rate_4: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz4", coerce=True, nullable=True, description="Hourly rate 4.")
48
+ hourly_rate_5: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz5", coerce=True, nullable=True, description="Hourly rate 5.")
49
+ hourly_rate_6: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz6", coerce=True, nullable=True, description="Hourly rate 6.")
50
+ hourly_rate_7: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz7", coerce=True, nullable=True, description="Hourly rate 7.")
51
+ hourly_rate_8: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz8", coerce=True, nullable=True, description="Hourly rate 8.")
52
+ hourly_rate_9: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz9", coerce=True, nullable=True, description="Hourly rate 9.")
53
+ hourly_rate_10: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="VereinbarteStundenloehne__Stundensatz10", coerce=True, nullable=True, description="Hourly rate 10.")
54
+
55
+ # Night/Sunday/Holiday allowances
56
+ automatic_base_wage_calculation: Optional[Series[bool]] = pa.Field(alias="SonnFeiertagNachtZuschlaege__AutomatischeGrundlohnermittlung", coerce=True, nullable=True, description="Automatic base wage calculation flag.")
57
+ base_hourly_wage: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="SonnFeiertagNachtZuschlaege__Grundstundenlohn", coerce=True, nullable=True, description="Base hourly wage.")
58
+
59
+ # Advance payments
60
+ automatic_advance_payment: Optional[Series[bool]] = pa.Field(alias="Abschlagszahlung__AutomatischerAbschlag", coerce=True, nullable=True, description="Automatic advance flag.")
61
+ advance_payment_amount: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Abschlagszahlung__Abschlag", coerce=True, nullable=True, description="Advance payment amount.")
62
+
63
+ # Overtime / absence config
64
+ overtime_calculation_id: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="UeberstundenFehlstunden__Berechnungsart__Id", coerce=True, nullable=True, description="Overtime calculation identifier.")
65
+ overtime_calculation_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="UeberstundenFehlstunden__Berechnungsart__Text", coerce=True, nullable=True, description="Overtime calculation text.")
66
+ overtime_calculation_id_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="UeberstundenFehlstunden__Berechnungsart__IdMitText", coerce=True, nullable=True, description="Overtime calculation id-text.")
67
+ overtime_payroll_code: Optional[Series[pd.StringDtype]] = pa.Field(alias="UeberstundenFehlstunden__LohnartUeberstunden", coerce=True, nullable=True, description="Overtime payroll code.")
68
+ absence_payroll_code: Optional[Series[pd.StringDtype]] = pa.Field(alias="UeberstundenFehlstunden__LohnartFehlstunden", coerce=True, nullable=True, description="Absence payroll code.")
69
+ employer_prepayment_payroll_code: Optional[Series[pd.StringDtype]] = pa.Field(alias="UeberstundenFehlstunden__LohnartFehlstundenArbeitgeberVorausleistung", coerce=True, nullable=True, description="Employer prepayment payroll code.")
70
+ additional_overtime_threshold: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="UeberstundenFehlstunden__ZusaetzlicheUeberstundenAb", coerce=True, nullable=True, description="Additional overtime threshold.")
71
+ additional_overtime_payroll_code: Optional[Series[pd.StringDtype]] = pa.Field(alias="UeberstundenFehlstunden__LohnartZusaetzlicheUeberstunden", coerce=True, nullable=True, description="Additional overtime payroll code.")
72
+ unpaid_absence_threshold: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="UeberstundenFehlstunden__UnbezahlteFehlstundenAb", coerce=True, nullable=True, description="Unpaid absence threshold.")
73
+ unpaid_absence_payroll_code: Optional[Series[pd.StringDtype]] = pa.Field(alias="UeberstundenFehlstunden__LohnartUnbezahlteFehlstunden", coerce=True, nullable=True, description="Unpaid absence payroll code.")
74
+
75
+ class _Annotation:
76
+ primary_key = "combined_key"
77
+
78
+ class Config:
79
+ coerce = True
80
+ strict = "filter"
81
+
82
+
83
+ class SalaryImport(BaseModel):
84
+ """
85
+ Pydantic schema for gross salary (Bruttolohn) import payloads.
86
+ """
87
+
88
+ company_id: Union[int, str] = Field(alias="Mdnr", description="Company id (Mandant).", example=1)
89
+ employee_number: Union[int, str] = Field(alias="Annr", description="Employee number.", example=100)
90
+ year: Union[int, str] = Field(alias="AbrJahr", description="Payroll year.", example=2024)
91
+ month: Union[int, str] = Field(alias="AbrMon", description="Payroll month.", example=1)
92
+ day: Union[int, str] = Field(alias="Tag", description="Day of month.", example=15)
93
+ wage_type: Union[int, str] = Field(alias="Lanr", description="Wage type number.", example=1000)
94
+ quantity: Optional[Union[int, float, str]] = Field(default=None, alias="Anzahl", description="Quantity value.", example=8.0)
95
+ amount: Optional[Union[int, float, str]] = Field(default=None, alias="Betrag", description="Amount value.", example=150.00)
96
+ cost_center: Optional[Union[int, str]] = Field(default=None, alias="KoSt1", description="Cost center.", example="CC001")
97
+ cost_unit: Optional[Union[int, str]] = Field(default=None, alias="KoTr1", description="Cost unit.", example="CU001")
98
+ aa1: Optional[Union[int, str]] = Field(default=None, alias="AA1", description="Additional classification field.", example="A1")
99
+ surcharge: Optional[Union[int, float, str]] = Field(default=None, alias="Zuschlag", description="Surcharge value.", example=25.00)
100
+
101
+ model_config = ConfigDict(populate_by_name=True)
@@ -0,0 +1,194 @@
1
+ """
2
+ Schemas for Sage Germany employment period payloads.
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import List, Optional
7
+
8
+ import pandas as pd
9
+ import pandera as pa
10
+ from pandera.typing import Series
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+ from brynq_sdk_functions import BrynQPanderaDataFrameModel
14
+
15
+ __all__ = [
16
+ "StartEndDatesGet",
17
+ "StartEndDatesKey",
18
+ "StartEndDatesInterval",
19
+ "StartEndDatesEntryExit",
20
+ "StartEndDatesCreate",
21
+ ]
22
+
23
+
24
+ class StartEndDatesKey(BaseModel):
25
+ """
26
+ Employee key used by employment period endpoints.
27
+ """
28
+
29
+ company_id: int = Field(alias="MdNr", description="Company id (Mandant).", example=1)
30
+ employee_number: int = Field(alias="AnNr", description="Employee number.", example=210)
31
+ combined_key: Optional[str] = Field(
32
+ default=None,
33
+ alias="CombinedKey",
34
+ description="Optional combined key value.",
35
+ example="1_210",
36
+ )
37
+
38
+ model_config = ConfigDict(populate_by_name=True)
39
+
40
+
41
+ class StartEndDatesInterval(BaseModel):
42
+ """
43
+ Generic Id/Text object used for probation interval, contract limits, and reasons.
44
+ """
45
+
46
+ id: Optional[int] = Field(default=None, alias="Id", description="Identifier value.", example=1)
47
+ text: Optional[str] = Field(default=None, alias="Text", description="Display text value.", example="Monthly")
48
+
49
+ model_config = ConfigDict(populate_by_name=True)
50
+
51
+
52
+ class StartEndDatesEntryExit(BaseModel):
53
+ """
54
+ Single entry/exit record within the employment period payload.
55
+ """
56
+
57
+ key: StartEndDatesKey = Field(
58
+ alias="Key",
59
+ description="Employee key for the entry/exit item.",
60
+ )
61
+ id: Optional[int] = Field(default=None, alias="Id", description="Entry/exit identifier.", example=1)
62
+ entry: datetime = Field(alias="Eintritt", description="Employment start date.", example="2024-01-01T00:00:00")
63
+ exit: Optional[datetime] = Field(default=None, alias="Austritt", description="Employment end date.", example="2024-12-31T00:00:00")
64
+ limited: Optional[StartEndDatesInterval] = Field(
65
+ default=None,
66
+ alias="Befristet",
67
+ description="Limited contract metadata.",
68
+ json_schema_extra={"prefix": "entry_exit_limited_"},
69
+ )
70
+ exit_reason: Optional[StartEndDatesInterval] = Field(
71
+ default=None,
72
+ alias="AustrittGrund",
73
+ description="Exit reason metadata.",
74
+ json_schema_extra={"prefix": "entry_exit_exit_reason_"},
75
+ )
76
+ exit_custom_reason: Optional[StartEndDatesInterval] = Field(
77
+ default=None,
78
+ alias="AustrittBenutzerdefinierterGrund",
79
+ description="Custom exit reason metadata.",
80
+ json_schema_extra={"prefix": "entry_exit_exit_custom_reason_"},
81
+ )
82
+
83
+ model_config = ConfigDict(populate_by_name=True)
84
+
85
+
86
+ class StartEndDatesCreate(BaseModel):
87
+ """
88
+ Pydantic schema for POST /employeenew/person/beschaeftigungszeiten.
89
+ Accepts nested structure; helper converts flat prefixed keys to this shape.
90
+ """
91
+
92
+ key: StartEndDatesKey = Field(
93
+ alias="Key",
94
+ description="Employee key for the employment period record.",
95
+ json_schema_extra={"prefix": "key_"},
96
+ )
97
+ corporate_entry: Optional[datetime] = Field(
98
+ default=None,
99
+ alias="KonzernEintritt",
100
+ description="Corporate entry date.",
101
+ example="2024-01-01T00:00:00",
102
+ )
103
+ probation_number: Optional[int] = Field(
104
+ default=None,
105
+ alias="ProbezeitZahl",
106
+ description="Probation duration number.",
107
+ example=6,
108
+ )
109
+ probation_interval: Optional[StartEndDatesInterval] = Field(
110
+ default=None,
111
+ alias="ProbezeitIntervall",
112
+ description="Probation interval metadata.",
113
+ json_schema_extra={"prefix": "probation_interval_"},
114
+ )
115
+ probation_end: Optional[datetime] = Field(
116
+ default=None,
117
+ alias="Probezeit",
118
+ description="Probation end date.",
119
+ example="2024-07-01T00:00:00",
120
+ )
121
+ entry_exit_periods: Optional[List[StartEndDatesEntryExit]] = Field(
122
+ default=None,
123
+ alias="EinAustritte",
124
+ description="List of entry/exit records.",
125
+ example=[],
126
+ json_schema_extra={"prefix": "entry_exit_"},
127
+ )
128
+ service_time: Optional[datetime] = Field(
129
+ default=None,
130
+ alias="Dienstzeit",
131
+ description="Service time date.",
132
+ example="2024-01-01T00:00:00",
133
+ )
134
+ service_time_duration: Optional[str] = Field(
135
+ default=None,
136
+ alias="DienstzeitDauer",
137
+ description="Service time duration string.",
138
+ example="1 year",
139
+ )
140
+ employment_time: Optional[datetime] = Field(
141
+ default=None,
142
+ alias="Beschaeftigungszeit",
143
+ description="Employment time date.",
144
+ example="2024-01-01T00:00:00",
145
+ )
146
+ employment_time_duration: Optional[str] = Field(
147
+ default=None,
148
+ alias="BeschaeftigungszeitDauer",
149
+ description="Employment time duration string.",
150
+ example="1 year",
151
+ )
152
+ allowance_claim: Optional[StartEndDatesInterval] = Field(
153
+ default=None,
154
+ alias="AnspruchZuwendung",
155
+ description="Allowance claim metadata.",
156
+ json_schema_extra={"prefix": "allowance_claim_"},
157
+ )
158
+
159
+ model_config = ConfigDict(populate_by_name=True)
160
+
161
+
162
+ class StartEndDatesGet(BrynQPanderaDataFrameModel):
163
+ """
164
+ Pandera schema for employee start/end date records.
165
+ """
166
+
167
+ # Base identifiers
168
+ company_id: Series[pd.Int64Dtype] = pa.Field(alias="Key__MdNr", coerce=True, nullable=False, description="Company id.")
169
+ employee_number: Series[pd.Int64Dtype] = pa.Field(alias="Key__AnNr", coerce=True, nullable=False, description="Employee number.")
170
+ combined_key: Series[pd.StringDtype] = pa.Field(alias="Key__CombinedKey", coerce=True, nullable=False, description="Combined key.")
171
+ key_is_empty: Optional[Series[bool]] = pa.Field(alias="Key__IsEmpty", coerce=True, nullable=True, description="Key empty flag.")
172
+
173
+ # Employment metadata
174
+ corporate_entry: Optional[Series[pd.StringDtype]] = pa.Field(alias="KonzernEintritt", coerce=True, nullable=True, description="Corporate entry date.")
175
+ probation_number: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="ProbezeitZahl", coerce=True, nullable=True, description="Probation duration number.")
176
+ probation_interval: Optional[Series[pd.StringDtype]] = pa.Field(alias="ProbezeitIntervall", coerce=True, nullable=True, description="Probation interval.")
177
+ probation_end: Optional[Series[pd.StringDtype]] = pa.Field(alias="Probezeit", coerce=True, nullable=True, description="Probation end date.")
178
+
179
+ # Aggregated durations
180
+ service_time: Optional[Series[pd.StringDtype]] = pa.Field(alias="Dienstzeit", coerce=True, nullable=True, description="Service time string.")
181
+ service_time_duration: Optional[Series[pd.StringDtype]] = pa.Field(alias="DienstzeitDauer", coerce=True, nullable=True, description="Service time duration.")
182
+ employment_time: Optional[Series[pd.StringDtype]] = pa.Field(alias="Beschaeftigungszeit", coerce=True, nullable=True, description="Employment time string.")
183
+ employment_time_duration: Optional[Series[pd.StringDtype]] = pa.Field(alias="BeschaeftigungszeitDauer", coerce=True, nullable=True, description="Employment time duration.")
184
+ allowance_claim: Optional[Series[pd.StringDtype]] = pa.Field(alias="AnspruchZuwendung", coerce=True, nullable=True, description="Allowance claim info.")
185
+
186
+ # Entry/exit payload
187
+ entry_exit_payload: Optional[Series[pd.StringDtype]] = pa.Field(alias="EinAustritte", coerce=True, nullable=True, description="Entry/exit history payload.")
188
+
189
+ class _Annotation:
190
+ primary_key = "combined_key"
191
+
192
+ class Config:
193
+ coerce = True
194
+ strict = "filter"
@@ -0,0 +1,117 @@
1
+ """
2
+ Schemas for Sage Germany vacation account (Urlaubskonto) payloads.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import pandas as pd
8
+ import pandera as pa
9
+ from pandera.typing import Series
10
+
11
+ from brynq_sdk_functions import BrynQPanderaDataFrameModel
12
+
13
+
14
+ class VacationAccountGet(BrynQPanderaDataFrameModel):
15
+ """
16
+ Pandera schema for employee vacation account balances.
17
+ """
18
+
19
+ # Base identifiers
20
+ key_date: Series[pd.StringDtype] = pa.Field(alias="Key__Date", coerce=True, nullable=False, description="Snapshot date.")
21
+ company_id: Series[pd.Int64Dtype] = pa.Field(alias="Key__MdNr", coerce=True, nullable=False, description="Company id.")
22
+ employee_number: Series[pd.Int64Dtype] = pa.Field(alias="Key__AnNr", coerce=True, nullable=False, description="Employee number.")
23
+ combined_key: Series[pd.StringDtype] = pa.Field(alias="Key__CombinedKey", coerce=True, nullable=False, description="Combined key.")
24
+ key_is_empty: Optional[Series[bool]] = pa.Field(alias="Key__IsEmpty", coerce=True, nullable=True, description="Key empty flag.")
25
+
26
+ # Vacation table metadata
27
+ vacation_table_id: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Urlaubstabelle__Id", coerce=True, nullable=True, description="Vacation table id.")
28
+ vacation_table_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="Urlaubstabelle__Text", coerce=True, nullable=True, description="Vacation table text.")
29
+ vacation_table_label: Optional[Series[pd.StringDtype]] = pa.Field(alias="Urlaubstabelle__IdMitText", coerce=True, nullable=True, description="Vacation table label.")
30
+ is_previous_calendar_year: Optional[Series[bool]] = pa.Field(alias="VergangenesKalenderjahr", coerce=True, nullable=True, description="Indicates previous calendar year view.")
31
+
32
+ # Employee info
33
+ data_company_id: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Daten__EmployeeKey__MdNr", coerce=True, nullable=True, description="Data company id.")
34
+ data_employee_number: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Daten__EmployeeKey__AnNr", coerce=True, nullable=True, description="Data employee number.")
35
+ data_combined_key: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__EmployeeKey__CombinedKey", coerce=True, nullable=True, description="Data combined key.")
36
+ employee_first_name: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__EmployeeName__Firstname", coerce=True, nullable=True, description="Employee first name.")
37
+ employee_last_name: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__EmployeeName__Lastname", coerce=True, nullable=True, description="Employee last name.")
38
+ employee_full_name: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__EmployeeName__Fullname", coerce=True, nullable=True, description="Employee full name.")
39
+ unit: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__Unit", coerce=True, nullable=True, description="Unit label.")
40
+
41
+ # Annual entitlements and usage
42
+ entitlement_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchKJ", coerce=True, nullable=True, description="Current year entitlement.")
43
+ entitlement_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchVJ", coerce=True, nullable=True, description="Previous year entitlement.")
44
+ entitlement_total: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchGesamt", coerce=True, nullable=True, description="Total entitlement.")
45
+ taken_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__GenommenKJ", coerce=True, nullable=True, description="Taken days current year.")
46
+ taken_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__GenommenVJ", coerce=True, nullable=True, description="Taken days previous year.")
47
+ taken_total: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__GenommenGesamt", coerce=True, nullable=True, description="Taken days total.")
48
+ rest_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestKJ", coerce=True, nullable=True, description="Remaining current year.")
49
+ rest_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestVJ", coerce=True, nullable=True, description="Remaining previous year.")
50
+ rest_total: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestGesamt", coerce=True, nullable=True, description="Remaining total.")
51
+ rest_total_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestGesamtVJ", coerce=True, nullable=True, description="Remaining total previous year.")
52
+ rest_total_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestGesamtKJ", coerce=True, nullable=True, description="Remaining total current year.")
53
+
54
+ entitlement_base_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchGrundKJ", coerce=True, nullable=True, description="Base entitlement current year.")
55
+ entitlement_carried_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchVortragVJ", coerce=True, nullable=True, description="Carried over previous year.")
56
+ entitlement_carried_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchVortragKJ", coerce=True, nullable=True, description="Carried over current year.")
57
+
58
+ # Additional/special leave
59
+ entitlement_additional_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchZusatzKJ", coerce=True, nullable=True, description="Additional leave current year.")
60
+ entitlement_additional_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchZusatzVJ", coerce=True, nullable=True, description="Additional leave previous year.")
61
+ entitlement_additional_total: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchZusatzGesamt", coerce=True, nullable=True, description="Additional leave total.")
62
+ entitlement_additional_raw: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchZusatzKJohneKorrektur", coerce=True, nullable=True, description="Additional leave raw current year.")
63
+ entitlement_special_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AnspruchSonderUrlTgKJ", coerce=True, nullable=True, description="Special leave entitlement current year.")
64
+
65
+ taken_additional_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__GenommenZusatzKJ", coerce=True, nullable=True, description="Additional leave taken current year.")
66
+ taken_additional_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__GenommenZusatzVJ", coerce=True, nullable=True, description="Additional leave taken previous year.")
67
+ taken_additional_total_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__GenommenZusatzGesamtVJ", coerce=True, nullable=True, description="Additional leave taken total previous year.")
68
+ taken_additional_total_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__GenommenZusatzGesamtKJ", coerce=True, nullable=True, description="Additional leave taken total current year.")
69
+ taken_without_entitlement: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__GenommenOhneAnspruch", coerce=True, nullable=True, description="Leave taken without entitlement.")
70
+
71
+ rest_additional_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestZusatzKJ", coerce=True, nullable=True, description="Additional leave remaining current year.")
72
+ rest_additional_total_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestZusatzGesamtVJ", coerce=True, nullable=True, description="Additional leave remaining total previous year.")
73
+ rest_special_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestSonderurlaubKJ", coerce=True, nullable=True, description="Special leave remaining current year.")
74
+
75
+ # Expiration / compensation
76
+ expiration_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__VerfallKJ", coerce=True, nullable=True, description="Expiration current year.")
77
+ expiration_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__VerfallVJ", coerce=True, nullable=True, description="Expiration previous year.")
78
+ expiration_additional_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__VerfallZusatzKJ", coerce=True, nullable=True, description="Additional expiration current year.")
79
+ expiration_additional_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__VerfallZusatzVJ", coerce=True, nullable=True, description="Additional expiration previous year.")
80
+ expiration_disability_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__VerfallSchwerbehinderungKJ", coerce=True, nullable=True, description="Disability expiration current year.")
81
+ compensation_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AbgeltKJ", coerce=True, nullable=True, description="Compensation current year.")
82
+ compensation_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AbgeltVJ", coerce=True, nullable=True, description="Compensation previous year.")
83
+ compensation_total: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AbgeltGesamt", coerce=True, nullable=True, description="Compensation total.")
84
+ expiration_total: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__VerfallGesamt", coerce=True, nullable=True, description="Expiration total.")
85
+ compensation_expiration_total: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__AbgeltVerfallGesamt", coerce=True, nullable=True, description="Compensation + expiration total.")
86
+ rest_total_incl_expiration: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestGesamtInklVerfall", coerce=True, nullable=True, description="Remaining total including expiration.")
87
+ rest_total_incl_planned: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestInklGeplant", coerce=True, nullable=True, description="Remaining including planned.")
88
+
89
+ # Planning and info
90
+ planned_leave_total: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__UrlaubGeplantGesamt", coerce=True, nullable=True, description="Planned leave total.")
91
+ planned_leave_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__UrlaubBeantragtKJ", coerce=True, nullable=True, description="Planned leave current year.")
92
+ recorded_leave_current_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__UrlaubErfasstKJ", coerce=True, nullable=True, description="Recorded leave current year.")
93
+ rest_leave_requested_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestUrlaubBeantragtVJ", coerce=True, nullable=True, description="Remaining leave requested previous year.")
94
+ rest_leave_recorded_previous_year: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Daten__RestUrlaubErfasstVJ", coerce=True, nullable=True, description="Remaining leave recorded previous year.")
95
+ expiration_date: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__UrlaubsVerfallsDatum", coerce=True, nullable=True, description="Leave expiration date.")
96
+ vacation_table_id_value: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Daten__UrlTabId", coerce=True, nullable=True, description="Vacation table id reference.")
97
+ vacation_table_name_value: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__UrlTabName", coerce=True, nullable=True, description="Vacation table name reference.")
98
+ keep_rest_current_year: Optional[Series[bool]] = pa.Field(alias="Daten__ResturlaubNichtBehaltenAktuellesJahr", coerce=True, nullable=True, description="Rest leave not retained current year.")
99
+ keep_rest_next_year: Optional[Series[bool]] = pa.Field(alias="Daten__ResturlaubNichtBehaltenFolgejahr", coerce=True, nullable=True, description="Rest leave not retained next year.")
100
+ expires_in_previous_year: Optional[Series[bool]] = pa.Field(alias="Daten__VerfaelltImVorjahr", coerce=True, nullable=True, description="Expires in previous year flag.")
101
+ rest_previous_year_auto: Optional[Series[bool]] = pa.Field(alias="Daten__RestVJAutomatischErmittelt", coerce=True, nullable=True, description="Rest previous year auto calculated.")
102
+ reference_year: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Daten__Year", coerce=True, nullable=True, description="Reference year.")
103
+ info_disability: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__InfoSchwerbehinderung", coerce=True, nullable=True, description="Disability info.")
104
+ info_base_entitlement: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__InfoGrundanspruch", coerce=True, nullable=True, description="Base entitlement info.")
105
+ info_additional_leave: Optional[Series[pd.StringDtype]] = pa.Field(alias="Daten__InfoZusatzurlaub", coerce=True, nullable=True, description="Additional leave info.")
106
+ recalculation_status: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Daten__Neuberechnungsstatus", coerce=True, nullable=True, description="Recalculation status.")
107
+
108
+ # Flags
109
+ has_vacation_table: Optional[Series[bool]] = pa.Field(alias="HatUrlaubstabelle", coerce=True, nullable=True, description="Has vacation table flag.")
110
+ has_disability_additional_leave: Optional[Series[bool]] = pa.Field(alias="HatZusatzurlaubSchwerbehinderung", coerce=True, nullable=True, description="Has disability additional leave.")
111
+
112
+ class _Annotation:
113
+ primary_key = "combined_key"
114
+
115
+ class Config:
116
+ coerce = True
117
+ strict = "filter"
@@ -0,0 +1,94 @@
1
+ """
2
+ Schemas for Sage Germany work hours payloads.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import pandas as pd
8
+ import pandera as pa
9
+ from pandera.typing import Series
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+
12
+ from brynq_sdk_functions import BrynQPanderaDataFrameModel
13
+
14
+ __all__ = ["WorkHoursGet", "WorkHoursCreate"]
15
+
16
+
17
+ class WorkHoursCreate(BaseModel):
18
+ """
19
+ Pydantic schema for POST /employeenew/person/arbeitszeiten.
20
+ """
21
+
22
+ company_id: int = Field(alias="MdNr", description="Company id (Mandant).", example=1)
23
+ employee_number: int = Field(alias="AnNr", description="Employee number.", example=100)
24
+ valid_from: str = Field(alias="ValidFrom", description="Validity start date ISO-8601.", example="2024-01-01T00:00:00")
25
+
26
+ work_schedule_label: Optional[str] = Field(default=None, alias="AnAzTab", description="Work schedule label.", example="STANDARD")
27
+ monthly_hours: Optional[float] = Field(default=None, alias="AnAzMon", description="Monthly hours.", example=160.0)
28
+ weekly_hours_average: Optional[float] = Field(default=None, alias="AnAzWoDuchschnitt", description="Average weekly hours.", example=40.0)
29
+ has_deviation_from_base: Optional[bool] = Field(
30
+ default=None, alias="AbweichendeBasisWochenStunden", description="Deviation from base weekly hours flag.", example=False
31
+ )
32
+ base_weekly_hours: Optional[float] = Field(default=None, alias="BasisWochenStunden", description="Base weekly hours.", example=40.0)
33
+ days_per_week: Optional[float] = Field(default=None, alias="TageProWoche", description="Days per week.", example=5.0)
34
+ is_industrial: Optional[bool] = Field(default=None, alias="IstGewerblich", description="Industrial employee flag.", example=False)
35
+
36
+ work_schedule_table_id: Optional[int] = Field(default=None, alias="Arbeitszeittabelle.Id", description="Schedule table id.", example=1)
37
+ work_schedule_table_text: Optional[str] = Field(default=None, alias="Arbeitszeittabelle.Text", description="Schedule table text.", example="Standard Schedule")
38
+ work_schedule_table_weeks: Optional[int] = Field(
39
+ default=None, alias="Arbeitszeittabelle.AnzahlWochen", description="Schedule table week count.", example=1
40
+ )
41
+ work_schedule_table_valid_from: Optional[str] = Field(
42
+ default=None, alias="ArbeitszeittabelleGueltigAb", description="Schedule table valid-from date.", example="2024-01-01"
43
+ )
44
+
45
+ part_time_factor: Optional[float] = Field(default=None, alias="Teilzeitfaktor", description="Part-time factor.", example=1.0)
46
+ change_reason_id: Optional[int] = Field(default=None, alias="AenderungsGrund.Id", description="Change reason id.", example=1)
47
+ change_reason_text: Optional[str] = Field(default=None, alias="AenderungsGrund.Text", description="Change reason text.", example="Initial Setup")
48
+ work_time_changed: Optional[bool] = Field(default=None, alias="ArbeitszeitHatSichGeaendert", description="Work time changed flag.", example=False)
49
+
50
+ model_config = ConfigDict(populate_by_name=True)
51
+
52
+
53
+ class WorkHoursGet(BrynQPanderaDataFrameModel):
54
+ """
55
+ Pandera schema for employee work hours (Arbeitszeiten) data.
56
+ """
57
+
58
+ company_id: Series[pd.Int64Dtype] = pa.Field(alias="MdNr", coerce=True, nullable=False, description="Company id.")
59
+ employee_number: Series[pd.Int64Dtype] = pa.Field(alias="AnNr", coerce=True, nullable=False, description="Employee number.")
60
+ combined_key: Series[pd.StringDtype] = pa.Field(alias="combined_key", coerce=True, nullable=False, description="Synthetic MdNr_AnNr combined key.")
61
+ valid_from: Optional[Series[pd.StringDtype]] = pa.Field(alias="ValidFrom", coerce=True, nullable=True, description="Validity start date.")
62
+ work_schedule_label: Optional[Series[pd.StringDtype]] = pa.Field(alias="AnAzTab", coerce=True, nullable=True, description="Work schedule label.")
63
+ monthly_hours: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="AnAzMon", coerce=True, nullable=True, description="Monthly hours.")
64
+ weekly_hours_average: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="AnAzWoDuchschnitt", coerce=True, nullable=True, description="Average weekly hours.")
65
+ has_deviation_from_base: Optional[Series[bool]] = pa.Field(alias="AbweichendeBasisWochenStunden", coerce=True, nullable=True, description="Deviation from base weekly hours flag.")
66
+ base_weekly_hours: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="BasisWochenStunden", coerce=True, nullable=True, description="Base weekly hours.")
67
+ days_per_week: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="TageProWoche", coerce=True, nullable=True, description="Days per week.")
68
+ is_industrial: Optional[Series[bool]] = pa.Field(alias="IstGewerblich", coerce=True, nullable=True, description="Industrial employee flag.")
69
+
70
+ work_schedule_table_id: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Arbeitszeittabelle__Id", coerce=True, nullable=True, description="Work schedule table id.")
71
+ work_schedule_table_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="Arbeitszeittabelle__Text", coerce=True, nullable=True, description="Work schedule table text.")
72
+ work_schedule_table_weeks: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Arbeitszeittabelle__AnzahlWochen", coerce=True, nullable=True, description="Work schedule table week count.")
73
+ work_schedule_table_valid_from: Optional[Series[pd.StringDtype]] = pa.Field(alias="ArbeitszeittabelleGueltigAb", coerce=True, nullable=True, description="Work schedule table valid-from date.")
74
+
75
+ part_time_factor: Optional[Series[pd.Float64Dtype]] = pa.Field(alias="Teilzeitfaktor", coerce=True, nullable=True, description="Part-time factor.")
76
+ change_reason_id: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="AenderungsGrund__Id", coerce=True, nullable=True, description="Change reason id.")
77
+ change_reason_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="AenderungsGrund__Text", coerce=True, nullable=True, description="Change reason text.")
78
+ change_reason_id_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="AenderungsGrund__IdMitText", coerce=True, nullable=True, description="Change reason id-text.")
79
+ work_time_changed: Optional[Series[bool]] = pa.Field(alias="ArbeitszeitHatSichGeaendert", coerce=True, nullable=True, description="Work time changed flag.")
80
+
81
+ calendar_id: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Kalender__Id", coerce=True, nullable=True, description="Calendar id.")
82
+ calendar_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="Kalender__Text", coerce=True, nullable=True, description="Calendar text.")
83
+ calendar_id_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="Kalender__IdMitText", coerce=True, nullable=True, description="Calendar id-text.")
84
+ tenant_calendar_id: Optional[Series[pd.Int64Dtype]] = pa.Field(alias="Mandantenkalender__Id", coerce=True, nullable=True, description="Tenant calendar id.")
85
+ tenant_calendar_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="Mandantenkalender__Text", coerce=True, nullable=True, description="Tenant calendar text.")
86
+ tenant_calendar_id_text: Optional[Series[pd.StringDtype]] = pa.Field(alias="Mandantenkalender__IdMitText", coerce=True, nullable=True, description="Tenant calendar id-text.")
87
+ calendar_source: Optional[Series[pd.StringDtype]] = pa.Field(alias="KalenderQuelle", coerce=True, nullable=True, description="Calendar source.")
88
+
89
+ class _Annotation:
90
+ primary_key = "combined_key"
91
+
92
+ class Config:
93
+ coerce = True
94
+ strict = "filter"
@@ -0,0 +1,123 @@
1
+ """
2
+ Start/end date 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, Optional, Tuple
9
+
10
+ import pandas as pd
11
+ from brynq_sdk_functions import Functions
12
+ from pydantic import BaseModel
13
+
14
+ from .helpers import start_end_flat_to_nested
15
+ from .schemas.start_end_dates import StartEndDatesCreate, StartEndDatesGet
16
+
17
+ if TYPE_CHECKING:
18
+ from .employees import Employees
19
+
20
+
21
+ class StartEndDates:
22
+ """
23
+ Handles employment period operations scoped to employees.
24
+ """
25
+
26
+ def __init__(self, sage) -> None:
27
+ self.sage = sage
28
+ self.base_url = "/employeenew/person/beschaeftigungszeiten"
29
+
30
+ def get(
31
+ self,
32
+ date: Optional[str] = None,
33
+ company_id: Optional[int] = None,
34
+ employee_number: Optional[int] = None,
35
+ ) -> Tuple[pd.DataFrame, pd.DataFrame]:
36
+ """
37
+ Retrieve employment period data for one or many employees.
38
+ """
39
+ try:
40
+ if company_id is not None and employee_number is not None:
41
+ employee_keys = [{"MdNr": company_id, "AnNr": employee_number}]
42
+ else:
43
+ employee_keys = self.sage._employee_search()
44
+
45
+ if not employee_keys:
46
+ return pd.DataFrame(), pd.DataFrame()
47
+
48
+ period_records = []
49
+ effective_date = date or datetime.now(timezone.utc).strftime("%Y-%m-%dT00:00:00")
50
+
51
+ for key in employee_keys:
52
+ response = self.sage.get(
53
+ path=self.base_url,
54
+ params={"MdNr": key["MdNr"], "AnNr": key["AnNr"], "date": effective_date},
55
+ )
56
+ response.raise_for_status()
57
+ payload = response.json()
58
+ if isinstance(payload, dict):
59
+ period_records.append(payload)
60
+
61
+ if not period_records:
62
+ return pd.DataFrame(), pd.DataFrame()
63
+
64
+ dataframe = pd.json_normalize(period_records, sep="__")
65
+
66
+ valid_data, invalid_data = Functions.validate_data(
67
+ df=dataframe,
68
+ schema=StartEndDatesGet,
69
+ )
70
+ return valid_data, invalid_data
71
+ except Exception as exc:
72
+ raise RuntimeError("Failed to retrieve employment period data.") from exc
73
+
74
+ def create(self, data: Dict[str, Any]) -> Dict[str, Any]:
75
+ """
76
+ Create or update employment period (Beschaeftigungszeiten) for an employee.
77
+
78
+ Accepts flat snake_case keys; converts to nested Sage payload using schema prefixes.
79
+ """
80
+ try:
81
+ nested_payload = start_end_flat_to_nested(data, StartEndDatesCreate)
82
+ validated = StartEndDatesCreate(**nested_payload)
83
+ payload = validated.model_dump(by_alias=True, exclude_none=True, mode="json")
84
+
85
+ response = self.sage.post(
86
+ path=self.base_url,
87
+ body=payload,
88
+ )
89
+ response.raise_for_status()
90
+ return response
91
+ except Exception as exc:
92
+ raise RuntimeError("Failed to create Sage Germany employment period.") from exc
93
+
94
+ def delete(
95
+ self,
96
+ company_id: int,
97
+ employee_number: int,
98
+ entry_date: str,
99
+ combined_key: Optional[str] = None,
100
+ ) -> Dict[str, Any]:
101
+ """
102
+ Delete an employment period by MdNr/AnNr/Eintritt (query params).
103
+ """
104
+ params: Dict[str, Any] = {
105
+ "MdNr": company_id,
106
+ "AnNr": employee_number,
107
+ "Eintritt": entry_date,
108
+ }
109
+ if combined_key is not None:
110
+ params["CombinedKey"] = combined_key
111
+
112
+ try:
113
+ response = self.sage.delete(
114
+ path=self.base_url,
115
+ params=params,
116
+ )
117
+ response.raise_for_status()
118
+ try:
119
+ return response.json()
120
+ except Exception:
121
+ return {"raw_response": response.text}
122
+ except Exception as exc:
123
+ raise RuntimeError("Failed to delete Sage Germany employment period.") from exc