Repository: bo4e/BO4E-python (language: python)

Issue:
Implement COM Ausschreibungsdetail
https://www.bo4e.de/dokumentation/komponenten/com-ausschreibungsdetail


Discussion / hints:

Fix patch:
diff --git a/src/bo4e/com/ausschreibungsdetail.py b/src/bo4e/com/ausschreibungsdetail.py
new file mode 100644
index 000000000..2b7fa6355
--- /dev/null
+++ b/src/bo4e/com/ausschreibungsdetail.py
@@ -0,0 +1,113 @@
+"""
+Contains class Ausschreibungsdetail and corresponding marshmallow schema for de-/serialization
+"""
+
+from typing import Optional
+
+import attr
+from marshmallow import fields, post_load
+from marshmallow_enum import EnumField  # type:ignore[import]
+
+from bo4e.com.adresse import Adresse, AdresseSchema
+from bo4e.com.com import COM, COMSchema
+from bo4e.com.menge import Menge, MengeSchema
+from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
+from bo4e.enum.netzebene import Netzebene
+from bo4e.enum.zaehlertyp import Zaehlertyp
+
+
+# pylint: disable=too-few-public-methods, too-many-instance-attributes
+@attr.s(auto_attribs=True, kw_only=True)
+class Ausschreibungsdetail(COM):
+    """
+    Die Komponente Ausschreibungsdetail wird verwendet um die Informationen zu einer Abnahmestelle innerhalb eines
+    Ausschreibungsloses abzubilden.
+    """
+
+    # required attributes
+    #: Identifikation einer ausgeschriebenen Marktlokation
+    lokations_id: str = attr.ib(validator=attr.validators.instance_of(str))
+    #: In der angegebenen Netzebene wird die Marktlokation versorgt, z.B. MSP für Mittelspannung
+    netzebene_lieferung: str = attr.ib(validator=attr.validators.instance_of(Netzebene))
+    #: In der angegebenen Netzebene wird die Lokation gemessen, z.B. NSP für Niederspannung
+    netzebene_messung: str = attr.ib(validator=attr.validators.instance_of(Netzebene))
+    #: Die Adresse an der die Marktlokation sich befindet
+    lokationsadresse: Adresse = attr.ib(validator=attr.validators.instance_of(Adresse))
+    #: Angefragter Zeitraum für die ausgeschriebene Belieferung
+    lieferzeitraum: Zeitraum = attr.ib(validator=attr.validators.instance_of(Zeitraum))
+
+    # optional attributes
+    #: Bezeichnung des zuständigen Netzbetreibers, z.B. 'Stromnetz Hamburg GmbH'
+    netzbetreiber: Optional[str] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(str)), default=None
+    )
+    #: Bezeichnung des Kunden, der die Marktlokation nutzt
+    kunde: Optional[str] = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(str)), default=None)
+    #: Die Bezeichnung des Zählers an der Marktlokation
+    zaehlernummer: Optional[str] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(str)), default=None
+    )
+    #: Bezeichnung für die Lokation, z.B. 'Zentraler Einkauf, Hamburg'
+    lokationsbezeichnung: Optional[str] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(str)), default=None
+    )
+
+    #: Spezifikation, um welche Zählertechnik es sich im vorliegenden Fall handelt, z.B. Leistungsmessung
+    zaehlertechnik: Optional[Zaehlertyp] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Zaehlertyp)), default=None
+    )
+    lastgang_vorhanden: Optional[bool] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(bool)), default=None
+    )
+    """
+    Zeigt an, ob es zu der Marktlokation einen Lastgang gibt.
+    Falls ja, kann dieser abgerufen werden und daraus die Verbrauchswerte ermittelt werden
+    """
+
+    #: Prognosewert für die Jahresarbeit der ausgeschriebenen Lokation
+    prognose_jahresarbeit: Optional[Menge] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Menge)), default=None
+    )
+    #: Ein Prognosewert für die Arbeit innerhalb des angefragten Lieferzeitraums der ausgeschriebenen Lokation
+    prognose_arbeit_lieferzeitraum: Optional[Menge] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Menge)), default=None
+    )
+    #: Prognosewert für die abgenommene maximale Leistung der ausgeschriebenen Lokation
+    prognose_leistung: Optional[Menge] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Menge)), default=None
+    )
+    #: Die (evtl. abweichende) Rechnungsadresse
+    rechnungsadresse: Optional[Adresse] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Adresse)), default=None
+    )
+
+
+class AusschreibungsdetailSchema(COMSchema):
+    """
+    Schema for de-/serialization of Ausschreibungsdetail
+    """
+
+    # required attributes
+    lokations_id = fields.Str()
+    netzebene_lieferung = EnumField(Netzebene)
+    netzebene_messung = EnumField(Netzebene)
+    lokationsadresse = fields.Nested(AdresseSchema)
+    lieferzeitraum = fields.Nested(ZeitraumSchema)
+
+    # optional attributes
+    netzbetreiber = fields.Str(allow_none=True)
+    kunde = fields.Str(allow_none=True)
+    zaehlernummer = fields.Str(allow_none=True)
+    lokationsbezeichnung = fields.Str(allow_none=True)
+    zaehlertechnik = EnumField(Zaehlertyp, allow_none=True)
+    lastgang_vorhanden = fields.Boolean(allow_none=True)
+    prognose_jahresarbeit = fields.Nested(MengeSchema, allow_none=True)
+    prognose_arbeit_lieferzeitraum = fields.Nested(MengeSchema, allow_none=True)
+    prognose_leistung = fields.Nested(MengeSchema, allow_none=True)
+    rechnungsadresse = fields.Nested(AdresseSchema, allow_none=True)
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Ausschreibungsdetail:
+        """Deserialize JSON to Ausschreibungsdetail object"""
+        return Ausschreibungsdetail(**data)


Test patch:
diff --git a/tests/test_adresse.py b/tests/test_adresse.py
index 846d5f5b7..4ecffb62b 100644
--- a/tests/test_adresse.py
+++ b/tests/test_adresse.py
@@ -5,6 +5,15 @@
 from bo4e.com.adresse import Adresse, AdresseSchema
 from bo4e.enum.landescode import Landescode
 
+# can be imported by other tests
+example_adresse = Adresse(
+    ort="Grünwald",
+    landescode=Landescode.DE,  # type:ignore[attr-defined]
+    hausnummer="27A",
+    strasse="Nördliche Münchner Straße",
+    postleitzahl="82031",
+)
+
 
 class TestAddress:
     def test_serialization_strasse(self):
diff --git a/tests/test_ausschreibungsdetail.py b/tests/test_ausschreibungsdetail.py
new file mode 100644
index 000000000..80d72f700
--- /dev/null
+++ b/tests/test_ausschreibungsdetail.py
@@ -0,0 +1,146 @@
+from decimal import Decimal
+
+import pytest  # type:ignore[import]
+
+from bo4e.com.ausschreibungsdetail import Ausschreibungsdetail, AusschreibungsdetailSchema
+from bo4e.com.menge import Menge
+from bo4e.enum.mengeneinheit import Mengeneinheit
+from bo4e.enum.netzebene import Netzebene
+from bo4e.enum.zaehlertyp import Zaehlertyp
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+from tests.test_adresse import example_adresse  # type:ignore[import]
+from tests.test_menge import example_menge  # type:ignore[import]
+from tests.test_zeitraum import example_zeitraum  # type:ignore[import]
+
+
+class TestAusschreibungsdetail:
+    @pytest.mark.parametrize(
+        "ausschreibungsdetail, expected_json_dict",
+        [
+            pytest.param(
+                Ausschreibungsdetail(
+                    lokations_id="56789012345",
+                    netzebene_lieferung=Netzebene.MSP,
+                    netzebene_messung=Netzebene.NSP,
+                    lokationsadresse=example_adresse,
+                    lieferzeitraum=example_zeitraum,
+                    netzbetreiber="Stromnetz Hamburg GmbH",
+                    kunde="Dei Mudder ihr Kunde",
+                    zaehlernummer="1YSK4234092304",
+                    lokationsbezeichnung="Zentraler Einkauf, Hamburg",
+                    zaehlertechnik=Zaehlertyp.LEISTUNGSZAEHLER,
+                    lastgang_vorhanden=True,
+                    prognose_leistung=Menge(wert=Decimal(40), einheit=Mengeneinheit.KW),
+                    prognose_arbeit_lieferzeitraum=Menge(wert=Decimal(2500), einheit=Mengeneinheit.KWH),
+                    prognose_jahresarbeit=Menge(wert=Decimal(2500), einheit=Mengeneinheit.KWH),
+                    rechnungsadresse=example_adresse,
+                ),
+                {
+                    "netzbetreiber": "Stromnetz Hamburg GmbH",
+                    "lieferzeitraum": {
+                        "enddatum": None,
+                        "startdatum": None,
+                        "einheit": "TAG",
+                        "endzeitpunkt": None,
+                        "dauer": "5",
+                        "startzeitpunkt": None,
+                    },
+                    "zaehlertechnik": "LEISTUNGSZAEHLER",
+                    "kunde": "Dei Mudder ihr Kunde",
+                    "lokationsbezeichnung": "Zentraler Einkauf, Hamburg",
+                    "lokationsadresse": {
+                        "hausnummer": "27A",
+                        "adresszusatz": None,
+                        "postfach": None,
+                        "postleitzahl": "82031",
+                        "landescode": "DE",
+                        "ort": "Grünwald",
+                        "strasse": "Nördliche Münchner Straße",
+                        "coErgaenzung": None,
+                    },
+                    "rechnungsadresse": {
+                        "hausnummer": "27A",
+                        "adresszusatz": None,
+                        "postfach": None,
+                        "postleitzahl": "82031",
+                        "landescode": "DE",
+                        "ort": "Grünwald",
+                        "strasse": "Nördliche Münchner Straße",
+                        "coErgaenzung": None,
+                    },
+                    "zaehlernummer": "1YSK4234092304",
+                    "prognoseJahresarbeit": {"wert": "2500", "einheit": "KWH"},
+                    "netzebeneLieferung": "MSP",
+                    "lokationsId": "56789012345",
+                    "prognoseLeistung": {"wert": "40", "einheit": "KW"},
+                    "lastgangVorhanden": True,
+                    "netzebeneMessung": "NSP",
+                    "prognoseArbeitLieferzeitraum": {"wert": "2500", "einheit": "KWH"},
+                },
+            ),
+            pytest.param(
+                Ausschreibungsdetail(
+                    lokations_id="56789012345",
+                    netzebene_lieferung=Netzebene.MSP,
+                    netzebene_messung=Netzebene.NSP,
+                    lokationsadresse=example_adresse,
+                    lieferzeitraum=example_zeitraum,
+                    rechnungsadresse=example_adresse,
+                ),
+                {
+                    "zaehlernummer": None,
+                    "zaehlertechnik": None,
+                    "kunde": None,
+                    "lokationsbezeichnung": None,
+                    "lieferzeitraum": {
+                        "dauer": "5",
+                        "startdatum": None,
+                        "endzeitpunkt": None,
+                        "enddatum": None,
+                        "einheit": "TAG",
+                        "startzeitpunkt": None,
+                    },
+                    "lokationsadresse": {
+                        "landescode": "DE",
+                        "hausnummer": "27A",
+                        "strasse": "Nördliche Münchner Straße",
+                        "postleitzahl": "82031",
+                        "ort": "Grünwald",
+                        "adresszusatz": None,
+                        "postfach": None,
+                        "coErgaenzung": None,
+                    },
+                    "rechnungsadresse": {
+                        "landescode": "DE",
+                        "hausnummer": "27A",
+                        "strasse": "Nördliche Münchner Straße",
+                        "postleitzahl": "82031",
+                        "ort": "Grünwald",
+                        "adresszusatz": None,
+                        "postfach": None,
+                        "coErgaenzung": None,
+                    },
+                    "netzbetreiber": None,
+                    "netzebeneLieferung": "MSP",
+                    "prognoseArbeitLieferzeitraum": None,
+                    "netzebeneMessung": "NSP",
+                    "prognoseLeistung": None,
+                    "lastgangVorhanden": None,
+                    "prognoseJahresarbeit": None,
+                    "lokationsId": "56789012345",
+                },
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, ausschreibungsdetail: Ausschreibungsdetail, expected_json_dict: dict):
+        """
+        Test de-/serialisation of Ausschreibungsdetail
+        """
+        assert_serialization_roundtrip(ausschreibungsdetail, AusschreibungsdetailSchema(), expected_json_dict)
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Ausschreibungsdetail()
+
+        assert "missing 5 required" in str(excinfo.value)
+        # 'lokations_id', 'netzebene_lieferung', 'netzebene_messung', 'lokationsadresse', and 'lieferzeitraum'
diff --git a/tests/test_menge.py b/tests/test_menge.py
index 971f24858..d2b682ebb 100644
--- a/tests/test_menge.py
+++ b/tests/test_menge.py
@@ -5,16 +5,17 @@
 from bo4e.com.menge import Menge, MengeSchema
 from bo4e.enum.mengeneinheit import Mengeneinheit
 
+example_menge = Menge(wert=Decimal(3.41), einheit=Mengeneinheit.MWH)
+
 
 class TestMenge:
     def test_menge(self):
         """
         Test de-/serialisation of Menge (only has required attributes).
         """
-        menge = Menge(wert=Decimal(3.41), einheit=Mengeneinheit.MWH)
 
         schema = MengeSchema()
-        json_string = schema.dumps(menge, ensure_ascii=False)
+        json_string = schema.dumps(example_menge, ensure_ascii=False)
 
         assert "3.41" in json_string
         assert "MWH" in json_string
diff --git a/tests/test_zeitraum.py b/tests/test_zeitraum.py
index 7e1298bb3..16c2243e7 100644
--- a/tests/test_zeitraum.py
+++ b/tests/test_zeitraum.py
@@ -6,6 +6,11 @@
 from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
 from bo4e.enum.zeiteinheit import Zeiteinheit
 
+example_zeitraum = Zeitraum(
+    einheit=Zeiteinheit.TAG,
+    dauer=Decimal(5),
+)
+
 
 class TestZeitraum:
     def test_zeitraum_dauer(self):