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

Issue:
Implement BO PreisblattHardware
https://www.bo4e.de/dokumentation/geschaeftsobjekte/preisblatthardware/24-11-2021
- #200 
- #181 


Discussion / hints:
fast identisch zu #204 

Fix patch:
diff --git a/src/bo4e/bo/preisblatthardware.py b/src/bo4e/bo/preisblatthardware.py
new file mode 100644
index 000000000..dc2c1b1e7
--- /dev/null
+++ b/src/bo4e/bo/preisblatthardware.py
@@ -0,0 +1,71 @@
+"""
+Contains PreisblattHardware class and corresponding marshmallow schema for de-/serialization
+"""
+from typing import List, Optional
+
+import attr
+from marshmallow import fields
+from marshmallow_enum import EnumField  # type:ignore[import]
+
+from bo4e.bo.preisblatt import Preisblatt, PreisblattSchema
+from bo4e.com.geraeteeigenschaften import Geraeteeigenschaften, GeraeteeigenschaftenSchema
+from bo4e.enum.bilanzierungsmethode import Bilanzierungsmethode
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.dienstleistungstyp import Dienstleistungstyp
+from bo4e.enum.netzebene import Netzebene
+
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class PreisblattHardware(Preisblatt):
+    """
+    Variante des Preisblattmodells zur Abbildung der Preise für zusätzliche Hardware
+    """
+
+    bo_typ: BoTyp = attr.ib(default=BoTyp.PREISBLATTHARDWARE)
+    # required attributes (additional to those of Preisblatt)
+    #: Die Preise gelten für Marktlokationen der angebebenen Bilanzierungsmethode
+    bilanzierungsmethode: Bilanzierungsmethode = attr.ib(validator=attr.validators.instance_of(Bilanzierungsmethode))
+    #: Die Preise gelten für Messlokationen in der angebebenen Netzebene
+    messebene: Netzebene = attr.ib(validator=attr.validators.instance_of(Netzebene))
+
+    #: Der Preis betriftt das hier angegebene Gerät, z.B. ein Tarifschaltgerät
+    basisgeraet: Geraeteeigenschaften = attr.ib(validator=attr.validators.instance_of(Geraeteeigenschaften))
+
+    # optional attributes
+    #: Im Preis sind die hier angegebenen Dienstleistungen enthalten, z.B. Jährliche Ablesung
+    inklusive_dienstleistungen: Optional[List[Dienstleistungstyp]] = attr.ib(
+        default=None,
+        validator=attr.validators.optional(
+            attr.validators.deep_iterable(
+                member_validator=attr.validators.instance_of(Dienstleistungstyp),
+                iterable_validator=attr.validators.instance_of(list),
+            )
+        ),
+    )
+
+    #: Im Preis sind die hier angegebenen Geräte mit enthalten, z.B. ein Wandler
+    inklusive_geraete: Optional[List[Geraeteeigenschaften]] = attr.ib(
+        default=None,
+        validator=attr.validators.optional(
+            attr.validators.deep_iterable(
+                member_validator=attr.validators.instance_of(Geraeteeigenschaften),
+                iterable_validator=attr.validators.instance_of(list),
+            )
+        ),
+    )
+
+
+class PreisblattHardwareSchema(PreisblattSchema):
+    """
+    Schema for de-/serialization of PreisblattHardware
+    """
+
+    class_name = PreisblattHardware  # type:ignore[assignment]
+    # required attributes
+    bilanzierungsmethode = EnumField(Bilanzierungsmethode)
+    messebene = EnumField(Netzebene)
+    basisgeraet = fields.Nested(GeraeteeigenschaftenSchema)
+    # optional attributes
+    inklusive_dienstleistungen = fields.List(EnumField(Dienstleistungstyp))
+    inklusive_geraete = fields.List(fields.Nested(GeraeteeigenschaftenSchema))
diff --git a/src/bo4e/bo/preisblattmessung.py b/src/bo4e/bo/preisblattmessung.py
index 94d012fb7..06a329b6b 100644
--- a/src/bo4e/bo/preisblattmessung.py
+++ b/src/bo4e/bo/preisblattmessung.py
@@ -45,7 +45,7 @@ class PreisblattMessung(Preisblatt):
         ),
     )
 
-    #: Im Preis sind die hier angebebenen Geräte mit enthalten, z.B. ein Wandler
+    #: Im Preis sind die hier angegebenen Geräte mit enthalten, z.B. ein Wandler
     inklusive_geraete: Optional[List[Geraeteeigenschaften]] = attr.ib(
         default=None,
         validator=attr.validators.optional(
diff --git a/src/bo4e/enum/botyp.py b/src/bo4e/enum/botyp.py
index 217e3c103..dcc5e463f 100644
--- a/src/bo4e/enum/botyp.py
+++ b/src/bo4e/enum/botyp.py
@@ -26,6 +26,7 @@ class BoTyp(StrEnum):
     NETZNUTZUNGSRECHNUNG = "NETZNUTZUNGSRECHNUNG"
     PREISBLATT = "PREISBLATT"
     PREISBLATTDIENSTLEISTUNG = "PREISBLATTDIENSTLEISTUNG"
+    PREISBLATTHARDWARE = "PREISBLATTHARDWARE"
     PREISBLATTKONZESSIONSABGABE = "PREISBLATTKONZESSIONSABGABE"
     PREISBLATTMESSUNG = "PREISBLATTMESSUNG"
     PREISBLATTNETZNUTZUNG = "PREISBLATTNETZNUTZUNG"


Test patch:
diff --git a/tests/test_preisblatt_hardware.py b/tests/test_preisblatt_hardware.py
new file mode 100644
index 000000000..2b2bff0a3
--- /dev/null
+++ b/tests/test_preisblatt_hardware.py
@@ -0,0 +1,46 @@
+import pytest  # type:ignore[import]
+
+from bo4e.bo.preisblatthardware import PreisblattHardware, PreisblattHardwareSchema
+from bo4e.enum.bilanzierungsmethode import Bilanzierungsmethode
+from bo4e.enum.dienstleistungstyp import Dienstleistungstyp
+from bo4e.enum.netzebene import Netzebene
+from bo4e.enum.preisstatus import Preisstatus
+from bo4e.enum.sparte import Sparte
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+from tests.test_geraeteeigenschaften import example_geraeteeigenschaften  # type:ignore[import]
+from tests.test_marktteilnehmer import example_marktteilnehmer  # type:ignore[import]
+from tests.test_preisposition import example_preisposition  # type:ignore[import]
+from tests.test_zeitraum import example_zeitraum  # type:ignore[import]
+
+
+class TestPreisblattHardware:
+    @pytest.mark.parametrize(
+        "preisblatt_hardware",
+        [
+            pytest.param(
+                PreisblattHardware(
+                    bezeichnung="foo",
+                    sparte=Sparte.STROM,
+                    preisstatus=Preisstatus.ENDGUELTIG,
+                    preispositionen=[example_preisposition],
+                    gueltigkeit=example_zeitraum,
+                    herausgeber=example_marktteilnehmer,
+                    bilanzierungsmethode=Bilanzierungsmethode.TLP_GEMEINSAM,
+                    messebene=Netzebene.MSP,
+                    inklusive_dienstleistungen=[Dienstleistungstyp.AUSLESUNG_FERNAUSLESUNG_ZUSAETZLICH_MSB],
+                    basisgeraet=example_geraeteeigenschaften,
+                    inklusive_geraete=[example_geraeteeigenschaften],
+                )
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, preisblatt_hardware: PreisblattHardware):
+        """
+        Test de-/serialisation
+        """
+        assert_serialization_roundtrip(preisblatt_hardware, PreisblattHardwareSchema())
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = PreisblattHardware()
+        assert "missing 8 required" in str(excinfo.value)  # 5 from preisblatt + 3 from preisblatt hardware
diff --git a/tests/test_preisblatt_messung.py b/tests/test_preisblatt_messung.py
index d495880ad..d5ba3237c 100644
--- a/tests/test_preisblatt_messung.py
+++ b/tests/test_preisblatt_messung.py
@@ -13,7 +13,7 @@
 from tests.test_zeitraum import example_zeitraum  # type:ignore[import]
 
 
-class TestPreisblatt:
+class TestPreisblattMessung:
     @pytest.mark.parametrize(
         "preisblatt_messung",
         [