From d41c728e08944a2aae2fb5e7d8255e7552398085 Mon Sep 17 00:00:00 2001 From: Antonis Kalipetis Date: Thu, 14 May 2026 14:55:37 +0300 Subject: [PATCH] Add EX_BASE_05 current workforce query --- ergani/client.py | 32 +++ ergani/models.py | 213 +++++++++++++++++- tests/test_current_workforce.py | 367 ++++++++++++++++++++++++++++++++ 3 files changed, 611 insertions(+), 1 deletion(-) create mode 100644 tests/test_current_workforce.py diff --git a/ergani/client.py b/ergani/client.py index 52b9a26..223bd2b 100644 --- a/ergani/client.py +++ b/ergani/client.py @@ -11,6 +11,8 @@ CompanyOvertime, CompanyWeeklySchedule, CompanyWorkCard, + CurrentWorkforceRecord, + CurrentWorkforceRequest, SubmissionResponse, ) from ergani.utils import extract_error_message, normalize_base_url @@ -280,3 +282,33 @@ def get_services_list(self) -> Optional[Response]: """ return self._request("GET", "/WebServices/ServicesList", None) + + def get_current_workforce( + self, afm: Optional[str] = None + ) -> List[CurrentWorkforceRecord]: + """ + Fetches current workforce records from the Ergani API. + + Args: + afm (Optional[str]): Optional employee tax identification number filter. + ``None`` omits the ``afm`` key from the request payload. An empty + string is serialized as ``{"afm": ""}``. + + Returns: + List[CurrentWorkforceRecord]: Current workforce records, each wrapping + the raw response object returned by the API. + + Raises: + APIError: An error occurred while communicating with the Ergani API + AuthenticationError: Raised if there is an authentication error with the Ergani API + """ + + parameters = CurrentWorkforceRequest(afm=afm).serialize() + response = self._execute_service("EX_BASE_05", parameters) + + if not response: + return CurrentWorkforceRecord.parse_many(None) + + payload = response.json() + + return CurrentWorkforceRecord.parse_many(payload) diff --git a/ergani/models.py b/ergani/models.py index 19a8eb1..b8336e8 100644 --- a/ergani/models.py +++ b/ergani/models.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from dataclasses import dataclass, field from datetime import date, datetime, time -from typing import List, Literal, Optional, TypedDict +from typing import Any, Dict, List, Literal, Optional, TypedDict from ergani.typings import ( LateDeclarationJustificationType, @@ -21,6 +23,215 @@ ) +@dataclass(frozen=True) +class CurrentWorkforceRequest: + afm: str | None = None + + def serialize(self) -> Dict[str, Any]: + payload: Dict[str, Any] = {} + + if self.afm is not None: + payload["afm"] = self.afm + + return payload + + +@dataclass +class CurrentWorkforceRecord: + employee_tax_identification_number: str | None = None + employee_last_name: str | None = None + employee_first_name: str | None = None + employee_father_first_name: str | None = None + employee_mother_first_name: str | None = None + birth_date: str | None = None + sex: str | None = None + nationality: str | None = None + marital_status: str | None = None + number_of_children: int | None = None + tax_office: str | None = None + unemployment_card_code: str | None = None + social_security_registry_number: str | None = None + social_security_number: str | None = None + address: str | None = None + postal_code: str | None = None + phone_number: str | None = None + kallikratis_municipal_code: str | None = None + underage_work_book_number: str | None = None + identity_document_type: str | None = None + identity_document_number: str | None = None + identity_document_issuing_authority: str | None = None + identity_document_issue_date: str | None = None + residence_permit_installment: str | None = None + residence_permit_installment_number: str | None = None + residence_permit_approval: str | None = None + residence_permit_approval_number: str | None = None + residence_permit_visa: str | None = None + residence_permit_visa_number: str | None = None + branch_number: int | None = None + employment_start_date: str | None = None + specialty: str | None = None + employee_classification: str | None = None + profession_code: str | None = None + weekly_workdays: str | None = None + prior_experience: str | None = None + employment_relationship: str | None = None + responsible_position: str | None = None + employment_status: str | None = None + weekly_hours: str | None = None + working_schedule: str | None = None + break_schedule: str | None = None + workplace: str | None = None + workplace_comments: str | None = None + wage_payment_frequency: str | None = None + unpredictable_work_schedule: str | None = None + on_demand_days_and_hours: str | None = None + on_demand_minimum_notification: str | None = None + on_demand_notes: str | None = None + mandatory_training: str | None = None + applicable_collective_agreement: str | None = None + applicable_collective_agreement_comments: str | None = None + working_time_arrangement: str | None = None + working_time_arrangement_comments: str | None = None + gross_pay: str | None = None + hourly_pay: str | None = None + primary_insurance: str | None = None + supplementary_insurance: str | None = None + additional_insurance_benefits: str | None = None + trial_period: str | None = None + borrowing_company_tax_identification_number: str | None = None + change_date: str | None = None + education_level: str | None = None + professional_education: str | None = None + pc_provided: str | None = None + working_time_digital_organization: str | None = None + full_employment_hours: str | None = None + break_minutes: int | None = None + break_within_schedule: str | None = None + working_card: str | None = None + flexible_working_hours: str | None = None + last_modified_date: str | None = None + raw_payload: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def parse_many(cls, payload: Any) -> List[CurrentWorkforceRecord]: + workforce_payloads = cls._parse_payloads(payload) + + return [cls.parse(payload) for payload in workforce_payloads] + + @classmethod + def parse(cls, payload: Any) -> CurrentWorkforceRecord: + if not isinstance(payload, dict): + raise ValueError( + "Expected current workforce record payload to be an object" + ) + + return cls( + employee_tax_identification_number=payload.get("afm"), + employee_last_name=payload.get("Eponimo"), + employee_first_name=payload.get("Onoma"), + employee_father_first_name=payload.get("OnomaPatera"), + employee_mother_first_name=payload.get("OnomaMiteras"), + birth_date=payload.get("BirthDate"), + sex=payload.get("Sex"), + nationality=payload.get("Nationality"), + marital_status=payload.get("MaritalStatus"), + number_of_children=_parse_int(payload.get("NumChildren")), + tax_office=payload.get("Doy"), + unemployment_card_code=payload.get("CodeAnergias"), + social_security_registry_number=payload.get("AmIka"), + social_security_number=payload.get("Amka"), + address=payload.get("Dieythinsi"), + postal_code=payload.get("Tk"), + phone_number=payload.get("Tilefwno"), + kallikratis_municipal_code=payload.get("Kallikratis"), + underage_work_book_number=payload.get("ArVivliouAnilikou"), + identity_document_type=payload.get("TyposTaytotitas"), + identity_document_number=payload.get("ArTaytotitas"), + identity_document_issuing_authority=payload.get("EkdousaArxi"), + identity_document_issue_date=payload.get("DateEkdosis"), + residence_permit_installment=payload.get("ResPermitInst"), + residence_permit_installment_number=payload.get("ResPermitInstAr"), + residence_permit_approval=payload.get("ResPermitAp"), + residence_permit_approval_number=payload.get("ResPermitApAr"), + residence_permit_visa=payload.get("ResPermitVisa"), + residence_permit_visa_number=payload.get("ResPermitVisaAr"), + branch_number=_parse_int(payload.get("PararthmaAa")), + employment_start_date=payload.get("DateFrom"), + specialty=payload.get("Eidikothta"), + employee_classification=payload.get("asXaraktirismos"), + profession_code=payload.get("Step"), + weekly_workdays=payload.get("WeekDays"), + prior_experience=payload.get("Proipiresia"), + employment_relationship=payload.get("SxesiApasxolisis"), + responsible_position=payload.get("ResponsiblePosition"), + employment_status=payload.get("KathestosApasxolisis"), + weekly_hours=payload.get("WeekHours"), + working_schedule=payload.get("Orario"), + break_schedule=payload.get("Dialeimma"), + workplace=payload.get("ToposErgasias"), + workplace_comments=payload.get("ToposErgasiasComments"), + wage_payment_frequency=payload.get("XronosKatabolisApodoxwn"), + unpredictable_work_schedule=payload.get("MhProblepsimoProgrammaErgasias"), + on_demand_days_and_hours=payload.get("ParaggeliaHmeresHours"), + on_demand_minimum_notification=payload.get("ParaggeliaMinNotification"), + on_demand_notes=payload.get("ParaggeliaNotes"), + mandatory_training=payload.get("IpoxreotikiKatartisi"), + applicable_collective_agreement=payload.get("EfarmosteaSyllogikiSymbasi"), + applicable_collective_agreement_comments=payload.get( + "EfarmosteaSyllogikiSymbasiComments" + ), + working_time_arrangement=payload.get("Dieythetisi"), + working_time_arrangement_comments=payload.get("DieythetisiComments"), + gross_pay=payload.get("Apodoxes"), + hourly_pay=payload.get("HourApodoxes"), + primary_insurance=payload.get("KyriaAsfalisi"), + supplementary_insurance=payload.get("EpikourikiAsfalisi"), + additional_insurance_benefits=payload.get("ProsthetesAsfalistikesParoxes"), + trial_period=payload.get("TrialPeriod"), + borrowing_company_tax_identification_number=payload.get("BorrowCompanyAfm"), + change_date=payload.get("DateMetabolhs"), + education_level=payload.get("EpipedoMorfosis"), + professional_education=payload.get("ProfessionalEducation"), + pc_provided=payload.get("Pc"), + working_time_digital_organization=payload.get( + "WorkingTimeDigitalOrganization" + ), + full_employment_hours=payload.get("FullEmploymentHours"), + break_minutes=_parse_int(payload.get("DialeimmaMinutes")), + break_within_schedule=payload.get("DialeimmaEntosWrariou"), + working_card=payload.get("WorkingCard"), + flexible_working_hours=payload.get("EueliktoWrario"), + last_modified_date=payload.get("LastModifiedDate"), + raw_payload=dict(payload), + ) + + @classmethod + def _parse_payloads(cls, payload: Any) -> List[Dict[str, Any]]: + if payload is None: + return [] + + if isinstance(payload, dict) and "EX_BASE_05" in payload: + payload = payload["EX_BASE_05"] + + if isinstance(payload, dict) and "Cur" in payload: + payload = payload["Cur"] + + if not isinstance(payload, list): + raise ValueError("Expected current workforce response payload to be a list") + + return payload + + +def _parse_int(value: Any) -> int | None: + if value is None or value == "": + return None + + try: + return int(value) + except (TypeError, ValueError): + return None + + @dataclass class WorkCard: """ diff --git a/tests/test_current_workforce.py b/tests/test_current_workforce.py new file mode 100644 index 0000000..2479ffb --- /dev/null +++ b/tests/test_current_workforce.py @@ -0,0 +1,367 @@ +from unittest import TestCase +from unittest.mock import Mock, patch + +from requests.models import Response + +from ergani.client import ErganiClient +from ergani.models import CurrentWorkforceRecord, CurrentWorkforceRequest + +CURRENT_WORKFORCE_PAYLOAD = { + "EX_BASE_05": { + "Cur": [ + { + "afm": "000000001", + "Eponimo": "ΠΑΠΑΔΟΠΟΥΛΟΥ", + "Onoma": "ΑΝΝΑ", + "OnomaPatera": "ΝΙΚΟΣ", + "OnomaMiteras": "ΕΛΕΝΗ", + "BirthDate": "1991-03-07T00:00:00+02:00", + "Sex": "ΓΥΝΑΙΚΑ (1)", + "Nationality": "048-ΕΛΛΑΔΑ", + "MaritalStatus": "ΑΓΑΜΟΣ/Η (0)", + "NumChildren": "0", + "Doy": "1152-ΒΥΡΩΝΑ", + "CodeAnergias": None, + "AmIka": "000000001", + "Amka": "00000000001", + "Dieythinsi": "ΟΔΟΣ ΔΟΚΙΜΗΣ 1 ΑΘΗΝΑ", + "Tk": "16121", + "Tilefwno": "2100000000", + "ArVivliouAnilikou": None, + "TyposTaytotitas": "ΔAT-ΔΕΛΤΙΟ ΑΣΤΥΝΟΜΙΚΗΣ ΤΑΥΤΟΤΗΤΑΣ", + "ArTaytotitas": "ΑΑ000001", + "EkdousaArxi": "Α.Τ. ΑΘΗΝΩΝ", + "DateEkdosis": "2009-07-10T00:00:00+03:00", + "ResPermitInst": "ΟΧΙ (0)", + "ResPermitInstAr": None, + "ResPermitAp": "ΟΧΙ (0)", + "ResPermitApAr": None, + "ResPermitVisa": "ΟΧΙ (0)", + "ResPermitVisaAr": None, + "PararthmaAa": "0", + "DateFrom": "2021-07-01T00:00:00+03:00", + "Eidikothta": "ΑΝΑΛΥΤΕΣ ΣΥΣΤΗΜΑΤΩΝ", + "asXaraktirismos": "ΥΠΑΛΛΗΛΟΣ (1)", + "Step": "213100-ΑΝΑΛΥΤΕΣ ΣΥΣΤΗΜΑΤΩΝ", + "WeekDays": "5-ΗΜΕΡΗ (5)", + "Proipiresia": "0", + "SxesiApasxolisis": "ΑΟΡΙΣΤΟΥ ΧΡΟΝΟΥ (0)", + "ResponsiblePosition": "ΟΧΙ (1)", + "KathestosApasxolisis": "ΠΛΗΡΗΣ (0)", + "WeekHours": "40.0", + "Orario": "ΨΗΦΙΑΚΗ ΟΡΓΑΝΩΣΗ ΧΡΟΝΟΥ ΕΡΓΑΣΙΑΣ", + "Dialeimma": "ΨΗΦΙΑΚΗ ΟΡΓΑΝΩΣΗ ΧΡΟΝΟΥ ΕΡΓΑΣΙΑΣ", + "ToposErgasias": "ΠΑΡΑΡΤΗΜΑ ΕΡΓΟΔΟΤΗ (0)", + "ToposErgasiasComments": None, + "XronosKatabolisApodoxwn": "ΤΕΛΟΣ ΚΑΘΕ ΜΗΝΑ", + "MhProblepsimoProgrammaErgasias": "ΟΧΙ (0)", + "ParaggeliaHmeresHours": None, + "ParaggeliaMinNotification": None, + "ParaggeliaNotes": None, + "IpoxreotikiKatartisi": "ΟΧΙ (0)", + "EfarmosteaSyllogikiSymbasi": "ΟΧΙ (0)", + "EfarmosteaSyllogikiSymbasiComments": None, + "Dieythetisi": "ΟΧΙ (2)", + "DieythetisiComments": None, + "Apodoxes": "1000.00", + "HourApodoxes": "10.00", + "KyriaAsfalisi": "001-ΗΛΕΚΤΡΟΝΙΚΟΣ ΕΘΝΙΚΟΣ ΦΟΡΕΑΣ ΚΟΙΝΩΝΙΚΗΣ ΑΣΦΑΛΙΣΗΣ (e-ΕΦΚΑ)", + "EpikourikiAsfalisi": "001-ΚΛΑΔΟΣ ΕΠΙΚΟΥΡΙΚΗΣ ΑΣΦΑΛΙΣΗΣ e-ΕΦΚΑ", + "ProsthetesAsfalistikesParoxes": None, + "TrialPeriod": "ΟΧΙ (0)", + "BorrowCompanyAfm": None, + "DateMetabolhs": "2026-05-01T00:00:00+03:00", + "EpipedoMorfosis": "11-ΑΕΙ", + "ProfessionalEducation": "ΟΧΙ (0)", + "Pc": "ΝΑΙ (1)", + "WorkingTimeDigitalOrganization": "ΝΑΙ (1)", + "FullEmploymentHours": "40.0", + "DialeimmaMinutes": "20", + "DialeimmaEntosWrariou": "ΝΑΙ (1)", + "WorkingCard": "ΟΧΙ (0)", + "EueliktoWrario": "0", + "LastModifiedDate": "2026-05-01T00:00:00+03:00", + }, + { + "afm": "000000002", + "Eponimo": "ΔΗΜΗΤΡΙΟΥ", + "Onoma": "ΓΙΩΡΓΟΣ", + "OnomaPatera": "ΣΤΑΥΡΟΣ", + "OnomaMiteras": "ΜΑΡΙΑ", + "BirthDate": "1996-09-09T00:00:00+03:00", + "Sex": "ΑΝΔΡΑΣ (0)", + "Nationality": "048-ΕΛΛΑΔΑ", + "MaritalStatus": "ΑΓΑΜΟΣ/Η (0)", + "NumChildren": "0", + "Doy": "3231-Α ΛΑΡΙΣΑΣ", + "CodeAnergias": None, + "AmIka": "000000002", + "Amka": "00000000002", + "Dieythinsi": "ΟΔΟΣ ΔΟΚΙΜΗΣ 2 ΛΑΡΙΣΑ", + "Tk": "41335", + "Tilefwno": "2410000000", + "Kallikratis": "91000301-1ο ΔΙΑΜΕΡΙΣΜΑ ΛΑΡΙΣΗΣ", + "ArVivliouAnilikou": None, + "TyposTaytotitas": "ΔAT-ΔΕΛΤΙΟ ΑΣΤΥΝΟΜΙΚΗΣ ΤΑΥΤΟΤΗΤΑΣ", + "ArTaytotitas": "ΑΑ000002", + "EkdousaArxi": "Υ.Α. ΛΑΡΙΣΑΣ", + "DateEkdosis": "2011-04-26T00:00:00+03:00", + "ResPermitInst": "ΟΧΙ (0)", + "ResPermitInstAr": None, + "ResPermitAp": "ΟΧΙ (0)", + "ResPermitApAr": None, + "ResPermitVisa": "ΟΧΙ (0)", + "ResPermitVisaAr": None, + "PararthmaAa": "0", + "DateFrom": "2022-06-01T00:00:00+03:00", + "Eidikothta": "ΑΝΑΛΥΤΕΣ ΣΥΣΤΗΜΑΤΩΝ", + "asXaraktirismos": "ΥΠΑΛΛΗΛΟΣ (1)", + "Step": "213100-ΑΝΑΛΥΤΕΣ ΣΥΣΤΗΜΑΤΩΝ", + "WeekDays": "5-ΗΜΕΡΗ (5)", + "Proipiresia": "0", + "SxesiApasxolisis": "ΑΟΡΙΣΤΟΥ ΧΡΟΝΟΥ (0)", + "ResponsiblePosition": "ΟΧΙ (1)", + "KathestosApasxolisis": "ΠΛΗΡΗΣ (0)", + "WeekHours": "40.0", + "Orario": "ΨΗΦΙΑΚΗ ΟΡΓΑΝΩΣΗ ΧΡΟΝΟΥ ΕΡΓΑΣΙΑΣ", + "Dialeimma": "ΨΗΦΙΑΚΗ ΟΡΓΑΝΩΣΗ ΧΡΟΝΟΥ ΕΡΓΑΣΙΑΣ", + "ToposErgasias": "ΠΑΡΑΡΤΗΜΑ ΕΡΓΟΔΟΤΗ (0)", + "ToposErgasiasComments": None, + "XronosKatabolisApodoxwn": "ΤΕΛΟΣ ΚΑΘΕ ΜΗΝΑ", + "MhProblepsimoProgrammaErgasias": "ΟΧΙ (0)", + "ParaggeliaHmeresHours": None, + "ParaggeliaMinNotification": None, + "ParaggeliaNotes": None, + "IpoxreotikiKatartisi": "ΟΧΙ (0)", + "EfarmosteaSyllogikiSymbasi": "ΟΧΙ (0)", + "EfarmosteaSyllogikiSymbasiComments": None, + "Dieythetisi": "ΟΧΙ (2)", + "DieythetisiComments": None, + "Apodoxes": "1100.00", + "HourApodoxes": "11.00", + "KyriaAsfalisi": "001-ΗΛΕΚΤΡΟΝΙΚΟΣ ΕΘΝΙΚΟΣ ΦΟΡΕΑΣ ΚΟΙΝΩΝΙΚΗΣ ΑΣΦΑΛΙΣΗΣ (e-ΕΦΚΑ)", + "EpikourikiAsfalisi": "002-ΤΑΜΕΙΟ ΕΠΙΚΟΥΡΙΚΗΣ ΚΕΦΑΛΑΙΟΠΟΙΗΤΙΚΗΣ ΑΣΦΑΛΙΣΗΣ (ΤΕΚΑ)", + "ProsthetesAsfalistikesParoxes": None, + "TrialPeriod": "ΟΧΙ (0)", + "BorrowCompanyAfm": None, + "DateMetabolhs": "2026-05-01T00:00:00+03:00", + "EpipedoMorfosis": "11-ΑΕΙ", + "ProfessionalEducation": "ΟΧΙ (0)", + "Pc": "ΝΑΙ (1)", + "WorkingTimeDigitalOrganization": "ΝΑΙ (1)", + "FullEmploymentHours": "40.0", + "DialeimmaMinutes": "20", + "DialeimmaEntosWrariou": "ΝΑΙ (1)", + "WorkingCard": "ΟΧΙ (0)", + "EueliktoWrario": "0", + "LastModifiedDate": "2026-05-01T00:00:00+03:00", + }, + ] + } +} + + +class CurrentWorkforceTests(TestCase): + def test_current_workforce_request_omits_none_and_preserves_empty_string(self): + self.assertEqual(CurrentWorkforceRequest().serialize(), {}) + self.assertEqual(CurrentWorkforceRequest(afm="").serialize(), {"afm": ""}) + + def test_current_workforce_record_parse_many_reads_wrapped_response(self): + self.assertEqual( + CurrentWorkforceRecord.parse_many(CURRENT_WORKFORCE_PAYLOAD), + [ + CurrentWorkforceRecord( + employee_tax_identification_number="000000001", + employee_last_name="ΠΑΠΑΔΟΠΟΥΛΟΥ", + employee_first_name="ΑΝΝΑ", + employee_father_first_name="ΝΙΚΟΣ", + employee_mother_first_name="ΕΛΕΝΗ", + birth_date="1991-03-07T00:00:00+02:00", + sex="ΓΥΝΑΙΚΑ (1)", + nationality="048-ΕΛΛΑΔΑ", + marital_status="ΑΓΑΜΟΣ/Η (0)", + number_of_children=0, + tax_office="1152-ΒΥΡΩΝΑ", + unemployment_card_code=None, + social_security_registry_number="000000001", + social_security_number="00000000001", + address="ΟΔΟΣ ΔΟΚΙΜΗΣ 1 ΑΘΗΝΑ", + postal_code="16121", + phone_number="2100000000", + kallikratis_municipal_code=None, + underage_work_book_number=None, + identity_document_type="ΔAT-ΔΕΛΤΙΟ ΑΣΤΥΝΟΜΙΚΗΣ ΤΑΥΤΟΤΗΤΑΣ", + identity_document_number="ΑΑ000001", + identity_document_issuing_authority="Α.Τ. ΑΘΗΝΩΝ", + identity_document_issue_date="2009-07-10T00:00:00+03:00", + residence_permit_installment="ΟΧΙ (0)", + residence_permit_installment_number=None, + residence_permit_approval="ΟΧΙ (0)", + residence_permit_approval_number=None, + residence_permit_visa="ΟΧΙ (0)", + residence_permit_visa_number=None, + branch_number=0, + employment_start_date="2021-07-01T00:00:00+03:00", + specialty="ΑΝΑΛΥΤΕΣ ΣΥΣΤΗΜΑΤΩΝ", + employee_classification="ΥΠΑΛΛΗΛΟΣ (1)", + profession_code="213100-ΑΝΑΛΥΤΕΣ ΣΥΣΤΗΜΑΤΩΝ", + weekly_workdays="5-ΗΜΕΡΗ (5)", + prior_experience="0", + employment_relationship="ΑΟΡΙΣΤΟΥ ΧΡΟΝΟΥ (0)", + responsible_position="ΟΧΙ (1)", + employment_status="ΠΛΗΡΗΣ (0)", + weekly_hours="40.0", + working_schedule="ΨΗΦΙΑΚΗ ΟΡΓΑΝΩΣΗ ΧΡΟΝΟΥ ΕΡΓΑΣΙΑΣ", + break_schedule="ΨΗΦΙΑΚΗ ΟΡΓΑΝΩΣΗ ΧΡΟΝΟΥ ΕΡΓΑΣΙΑΣ", + workplace="ΠΑΡΑΡΤΗΜΑ ΕΡΓΟΔΟΤΗ (0)", + workplace_comments=None, + wage_payment_frequency="ΤΕΛΟΣ ΚΑΘΕ ΜΗΝΑ", + unpredictable_work_schedule="ΟΧΙ (0)", + on_demand_days_and_hours=None, + on_demand_minimum_notification=None, + on_demand_notes=None, + mandatory_training="ΟΧΙ (0)", + applicable_collective_agreement="ΟΧΙ (0)", + applicable_collective_agreement_comments=None, + working_time_arrangement="ΟΧΙ (2)", + working_time_arrangement_comments=None, + gross_pay="1000.00", + hourly_pay="10.00", + primary_insurance="001-ΗΛΕΚΤΡΟΝΙΚΟΣ ΕΘΝΙΚΟΣ ΦΟΡΕΑΣ ΚΟΙΝΩΝΙΚΗΣ ΑΣΦΑΛΙΣΗΣ (e-ΕΦΚΑ)", + supplementary_insurance="001-ΚΛΑΔΟΣ ΕΠΙΚΟΥΡΙΚΗΣ ΑΣΦΑΛΙΣΗΣ e-ΕΦΚΑ", + additional_insurance_benefits=None, + trial_period="ΟΧΙ (0)", + borrowing_company_tax_identification_number=None, + change_date="2026-05-01T00:00:00+03:00", + education_level="11-ΑΕΙ", + professional_education="ΟΧΙ (0)", + pc_provided="ΝΑΙ (1)", + working_time_digital_organization="ΝΑΙ (1)", + full_employment_hours="40.0", + break_minutes=20, + break_within_schedule="ΝΑΙ (1)", + working_card="ΟΧΙ (0)", + flexible_working_hours="0", + last_modified_date="2026-05-01T00:00:00+03:00", + raw_payload=CURRENT_WORKFORCE_PAYLOAD["EX_BASE_05"]["Cur"][0], + ), + CurrentWorkforceRecord( + employee_tax_identification_number="000000002", + employee_last_name="ΔΗΜΗΤΡΙΟΥ", + employee_first_name="ΓΙΩΡΓΟΣ", + employee_father_first_name="ΣΤΑΥΡΟΣ", + employee_mother_first_name="ΜΑΡΙΑ", + birth_date="1996-09-09T00:00:00+03:00", + sex="ΑΝΔΡΑΣ (0)", + nationality="048-ΕΛΛΑΔΑ", + marital_status="ΑΓΑΜΟΣ/Η (0)", + number_of_children=0, + tax_office="3231-Α ΛΑΡΙΣΑΣ", + unemployment_card_code=None, + social_security_registry_number="000000002", + social_security_number="00000000002", + address="ΟΔΟΣ ΔΟΚΙΜΗΣ 2 ΛΑΡΙΣΑ", + postal_code="41335", + phone_number="2410000000", + kallikratis_municipal_code="91000301-1ο ΔΙΑΜΕΡΙΣΜΑ ΛΑΡΙΣΗΣ", + underage_work_book_number=None, + identity_document_type="ΔAT-ΔΕΛΤΙΟ ΑΣΤΥΝΟΜΙΚΗΣ ΤΑΥΤΟΤΗΤΑΣ", + identity_document_number="ΑΑ000002", + identity_document_issuing_authority="Υ.Α. ΛΑΡΙΣΑΣ", + identity_document_issue_date="2011-04-26T00:00:00+03:00", + residence_permit_installment="ΟΧΙ (0)", + residence_permit_installment_number=None, + residence_permit_approval="ΟΧΙ (0)", + residence_permit_approval_number=None, + residence_permit_visa="ΟΧΙ (0)", + residence_permit_visa_number=None, + branch_number=0, + employment_start_date="2022-06-01T00:00:00+03:00", + specialty="ΑΝΑΛΥΤΕΣ ΣΥΣΤΗΜΑΤΩΝ", + employee_classification="ΥΠΑΛΛΗΛΟΣ (1)", + profession_code="213100-ΑΝΑΛΥΤΕΣ ΣΥΣΤΗΜΑΤΩΝ", + weekly_workdays="5-ΗΜΕΡΗ (5)", + prior_experience="0", + employment_relationship="ΑΟΡΙΣΤΟΥ ΧΡΟΝΟΥ (0)", + responsible_position="ΟΧΙ (1)", + employment_status="ΠΛΗΡΗΣ (0)", + weekly_hours="40.0", + working_schedule="ΨΗΦΙΑΚΗ ΟΡΓΑΝΩΣΗ ΧΡΟΝΟΥ ΕΡΓΑΣΙΑΣ", + break_schedule="ΨΗΦΙΑΚΗ ΟΡΓΑΝΩΣΗ ΧΡΟΝΟΥ ΕΡΓΑΣΙΑΣ", + workplace="ΠΑΡΑΡΤΗΜΑ ΕΡΓΟΔΟΤΗ (0)", + workplace_comments=None, + wage_payment_frequency="ΤΕΛΟΣ ΚΑΘΕ ΜΗΝΑ", + unpredictable_work_schedule="ΟΧΙ (0)", + on_demand_days_and_hours=None, + on_demand_minimum_notification=None, + on_demand_notes=None, + mandatory_training="ΟΧΙ (0)", + applicable_collective_agreement="ΟΧΙ (0)", + applicable_collective_agreement_comments=None, + working_time_arrangement="ΟΧΙ (2)", + working_time_arrangement_comments=None, + gross_pay="1100.00", + hourly_pay="11.00", + primary_insurance="001-ΗΛΕΚΤΡΟΝΙΚΟΣ ΕΘΝΙΚΟΣ ΦΟΡΕΑΣ ΚΟΙΝΩΝΙΚΗΣ ΑΣΦΑΛΙΣΗΣ (e-ΕΦΚΑ)", + supplementary_insurance="002-ΤΑΜΕΙΟ ΕΠΙΚΟΥΡΙΚΗΣ ΚΕΦΑΛΑΙΟΠΟΙΗΤΙΚΗΣ ΑΣΦΑΛΙΣΗΣ (ΤΕΚΑ)", + additional_insurance_benefits=None, + trial_period="ΟΧΙ (0)", + borrowing_company_tax_identification_number=None, + change_date="2026-05-01T00:00:00+03:00", + education_level="11-ΑΕΙ", + professional_education="ΟΧΙ (0)", + pc_provided="ΝΑΙ (1)", + working_time_digital_organization="ΝΑΙ (1)", + full_employment_hours="40.0", + break_minutes=20, + break_within_schedule="ΝΑΙ (1)", + working_card="ΟΧΙ (0)", + flexible_working_hours="0", + last_modified_date="2026-05-01T00:00:00+03:00", + raw_payload=CURRENT_WORKFORCE_PAYLOAD["EX_BASE_05"]["Cur"][1], + ), + ], + ) + + def test_current_workforce_record_parse_many_requires_list_payload(self): + with self.assertRaisesRegex(ValueError, "payload to be a list"): + CurrentWorkforceRecord.parse_many({"EX_BASE_05": {"Cur": {}}}) + + def test_get_current_workforce_uses_afm_filter_and_parses_wrapped_response(self): + client = ErganiClient("username", "password", "https://example.test") + response = Mock(spec=Response) + response.json.return_value = CURRENT_WORKFORCE_PAYLOAD + + with patch.object( + client, "_execute_service", return_value=response + ) as execute_service_mock: + result = client.get_current_workforce(afm="000000001") + + self.assertEqual( + result, CurrentWorkforceRecord.parse_many(CURRENT_WORKFORCE_PAYLOAD) + ) + execute_service_mock.assert_called_once_with("EX_BASE_05", {"afm": "000000001"}) + + def test_get_current_workforce_preserves_empty_string_filter(self): + client = ErganiClient("username", "password", "https://example.test") + response = Mock(spec=Response) + response.json.return_value = {"EX_BASE_05": {"Cur": []}} + + with patch.object( + client, "_execute_service", return_value=response + ) as execute_service_mock: + result = client.get_current_workforce(afm="") + + self.assertEqual(result, []) + execute_service_mock.assert_called_once_with("EX_BASE_05", {"afm": ""}) + + def test_get_current_workforce_omits_none_filter_and_handles_empty_response(self): + client = ErganiClient("username", "password", "https://example.test") + + with patch.object( + client, "_execute_service", return_value=None + ) as execute_service_mock: + result = client.get_current_workforce() + + self.assertEqual(result, []) + execute_service_mock.assert_called_once_with("EX_BASE_05", {})