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

Issue:
Implement BO Preisblatt
https://www.bo4e.de/dokumentation/geschaeftsobjekte/bo-preisblatt
- #182 
- #118 
Implement COM Preisposition + Enum ArtikelId
Fixes #182


Discussion / hints:

Fix patch:
diff --git a/src/bo4e/bo/preisblatt.py b/src/bo4e/bo/preisblatt.py
new file mode 100644
index 000000000..4a738bdf6
--- /dev/null
+++ b/src/bo4e/bo/preisblatt.py
@@ -0,0 +1,72 @@
+"""
+Contains Preisblatt 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.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
+from bo4e.bo.marktteilnehmer import Marktteilnehmer, MarktteilnehmerSchema
+from bo4e.com.preisposition import Preisposition, PreispositionSchema
+from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.preisstatus import Preisstatus
+from bo4e.enum.sparte import Sparte
+from bo4e.validators import check_list_length_at_least_one
+
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Preisblatt(Geschaeftsobjekt):
+    """
+    Das allgemeine Modell zur Abbildung von Preisen;
+    Davon abgeleitet können, über die Zuordnung identifizierender Merkmale, spezielle Preisblatt-Varianten modelliert
+    werden.
+
+    Die jeweiligen Sätze von Merkmalen sind in der Grafik ergänzt worden und stellen jeweils eine Ausprägung für die
+    verschiedenen Anwendungsfälle der Preisblätter dar.
+    """
+
+    # required attributes
+    bo_typ: BoTyp = attr.ib(default=BoTyp.PREISBLATT)
+    #: Eine Bezeichnung für das Preisblatt
+    bezeichnung: str = attr.ib(validator=attr.validators.instance_of(str))
+    #: Preisblatt gilt für angegebene Sparte
+    sparte: Sparte = attr.ib(validator=attr.validators.instance_of(Sparte))
+    #: Merkmal, das anzeigt, ob es sich um vorläufige oder endgültige Preise handelt
+    preisstatus: Preisstatus = attr.ib(validator=attr.validators.instance_of(Preisstatus))
+    #: Der Zeitraum für den der Preis festgelegt ist
+    gueltigkeit: Zeitraum = attr.ib(validator=attr.validators.instance_of(Zeitraum))
+    #: Die einzelnen Positionen, die mit dem Preisblatt abgerechnet werden können. Z.B. Arbeitspreis, Grundpreis etc
+    preispositionen: List[Preisposition] = attr.ib(
+        validator=attr.validators.deep_iterable(
+            member_validator=attr.validators.instance_of(Preisposition),
+            iterable_validator=check_list_length_at_least_one,
+        )
+    )
+    # optional attributes
+    #: Der Netzbetreiber, der die Preise veröffentlicht hat
+    herausgeber: Optional[Marktteilnehmer] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Marktteilnehmer))
+    )
+
+
+class PreisblattSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Preisblatt
+    """
+
+    class_name = Preisblatt
+    # required attributes
+    bezeichnung = fields.Str()
+    sparte = EnumField(Sparte)
+    preisstatus = EnumField(Preisstatus)
+    gueltigkeit = fields.Nested(ZeitraumSchema)
+    preispositionen = fields.List(fields.Nested(PreispositionSchema))
+
+    # optional attributes
+
+    herausgeber = fields.Nested(MarktteilnehmerSchema, load_default=None)


Test patch:
diff --git a/tests/test_preisblatt.py b/tests/test_preisblatt.py
new file mode 100644
index 000000000..6bd5eba6f
--- /dev/null
+++ b/tests/test_preisblatt.py
@@ -0,0 +1,37 @@
+import pytest  # type:ignore[import]
+
+from bo4e.bo.preisblatt import Preisblatt, PreisblattSchema
+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_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 TestPreisblatt:
+    @pytest.mark.parametrize(
+        "preisblatt",
+        [
+            pytest.param(
+                Preisblatt(
+                    bezeichnung="foo",
+                    sparte=Sparte.STROM,
+                    preisstatus=Preisstatus.ENDGUELTIG,
+                    preispositionen=[example_preisposition],
+                    gueltigkeit=example_zeitraum,
+                    herausgeber=example_marktteilnehmer,
+                )
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, preisblatt: Preisblatt):
+        """
+        Test de-/serialisation
+        """
+        assert_serialization_roundtrip(preisblatt, PreisblattSchema())
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Preisblatt()
+        assert "missing 5 required" in str(excinfo.value)
diff --git a/tests/test_preisposition.py b/tests/test_preisposition.py
index 85cac762c..b6f5447a8 100644
--- a/tests/test_preisposition.py
+++ b/tests/test_preisposition.py
@@ -15,28 +15,28 @@
 from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
 from tests.test_preisstaffel import example_preisstaffel  # type:ignore[import]
 
+example_preisposition = Preisposition(
+    berechnungsmethode=Kalkulationsmethode.AP_TRANSPORT_ODER_VERTEILNETZ,
+    leistungstyp=Leistungstyp.SONSTIGER_PREIS,
+    leistungsbezeichnung="Foo",
+    preiseinheit=Waehrungseinheit.EUR,
+    bezugsgroesse=Mengeneinheit.KVARH,
+    zeitbasis=Zeiteinheit.HALBJAHR,
+    tarifzeit=Tarifzeit.TZ_HT,
+    bdew_artikelnummer=BDEWArtikelnummer.AUSGLEICHSENERGIE_UNTERDECKUNG,
+    zonungsgroesse=Bemessungsgroesse.BENUTZUNGSDAUER,
+    freimenge_blindarbeit=Decimal(50),  # %
+    freimenge_leistungsfaktor=Decimal(1.0),
+    preisstaffeln=[example_preisstaffel],
+    artikel_id=ArtikelId.ARTIKEL_2017005,
+)
+
 
 class TestPreisposition:
     @pytest.mark.parametrize(
         "preisposition",
         [
-            pytest.param(
-                Preisposition(
-                    berechnungsmethode=Kalkulationsmethode.AP_TRANSPORT_ODER_VERTEILNETZ,
-                    leistungstyp=Leistungstyp.SONSTIGER_PREIS,
-                    leistungsbezeichnung="Foo",
-                    preiseinheit=Waehrungseinheit.EUR,
-                    bezugsgroesse=Mengeneinheit.KVARH,
-                    zeitbasis=Zeiteinheit.HALBJAHR,
-                    tarifzeit=Tarifzeit.TZ_HT,
-                    bdew_artikelnummer=BDEWArtikelnummer.AUSGLEICHSENERGIE_UNTERDECKUNG,
-                    zonungsgroesse=Bemessungsgroesse.BENUTZUNGSDAUER,
-                    freimenge_blindarbeit=Decimal(50),  # %
-                    freimenge_leistungsfaktor=Decimal(1.0),
-                    preisstaffeln=[example_preisstaffel],
-                    artikel_id=ArtikelId.ARTIKEL_2017003,
-                )
-            ),
+            pytest.param(example_preisposition),
         ],
     )
     def test_serialization_roundtrip(self, preisposition: Preisposition):