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

Issue:
Implement BO Tarifpreisblatt
https://www.bo4e.de/dokumentation/geschaeftsobjekte/bo-tarifpreisblatt
- #207 
- #186 
- #141 
- #176 
- #163 
- #162 


Discussion / hints:

Fix patch:
diff --git a/src/bo4e/bo/tarifpreisblatt.py b/src/bo4e/bo/tarifpreisblatt.py
new file mode 100644
index 000000000..734fedbce
--- /dev/null
+++ b/src/bo4e/bo/tarifpreisblatt.py
@@ -0,0 +1,77 @@
+"""
+Contains Tarifpreisblatt class and corresponding marshmallow schema for de-/serialization
+"""
+from datetime import datetime
+from typing import List, Optional
+
+import attr
+from marshmallow import fields
+
+from bo4e.bo.tarifinfo import Tarifinfo, TarifinfoSchema
+from bo4e.com.aufabschlag import AufAbschlag, AufAbschlagSchema
+from bo4e.com.preisgarantie import Preisgarantie, PreisgarantieSchema
+from bo4e.com.tarifberechnungsparameter import Tarifberechnungsparameter, TarifberechnungsparameterSchema
+from bo4e.com.tarifeinschraenkung import Tarifeinschraenkung, TarifeinschraenkungSchema
+from bo4e.com.tarifpreisposition import Tarifpreisposition, TarifpreispositionSchema
+from bo4e.enum.botyp import BoTyp
+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 Tarifpreisblatt(Tarifinfo):
+    """
+    Tarifinformation mit Preisen, Aufschlägen und Berechnungssystematik
+    """
+
+    bo_typ: BoTyp = attr.ib(default=BoTyp.TARIFPREISBLATT)
+    # required attributes (additional to those of Tarifinfo)
+    #: Gibt an, wann der Preis zuletzt angepasst wurde
+    preisstand: datetime = attr.ib(validator=attr.validators.instance_of(datetime))
+    #: Die festgelegten Preise, z.B. für Arbeitspreis, Grundpreis etc.
+    tarifpreise: List[Tarifpreisposition] = attr.ib(
+        validator=attr.validators.deep_iterable(
+            member_validator=attr.validators.instance_of(Tarifpreisposition),
+            iterable_validator=check_list_length_at_least_one,
+        )
+    )
+    #: Für die Berechnung der Kosten sind die hier abgebildeten Parameter heranzuziehen
+    berechnungsparameter: Tarifberechnungsparameter = attr.ib(
+        validator=attr.validators.instance_of(Tarifberechnungsparameter)
+    )
+
+    # optional attributes
+    #: Die Bedingungen und Einschränkungen unter denen ein Tarif angewendet werden kann
+    tarifeinschraenkung: Optional[Tarifeinschraenkung] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Tarifeinschraenkung))
+    )
+    #: Festlegung von Garantien für bestimmte Preisanteile
+    preisgarantie: Optional[Preisgarantie] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Preisgarantie))
+    )
+    #: Auf- und Abschläge auf die Preise oder Kosten
+    tarif_auf_abschlaege: Optional[List[AufAbschlag]] = attr.ib(
+        default=None,
+        validator=attr.validators.optional(
+            attr.validators.deep_iterable(
+                member_validator=attr.validators.instance_of(AufAbschlag),
+                iterable_validator=attr.validators.instance_of(list),
+            )
+        ),
+    )
+
+
+class TarifpreisblattSchema(TarifinfoSchema):
+    """
+    Schema for de-/serialization of Tarifpreisblatt
+    """
+
+    class_name = Tarifpreisblatt  # type:ignore[assignment]
+    # required attributes
+    preisstand = fields.DateTime()
+    berechnungsparameter = fields.Nested(TarifberechnungsparameterSchema)
+    tarifpreise = fields.List(fields.Nested(TarifpreispositionSchema))
+    # optional attributes
+    tarif_auf_abschlaege = fields.List(fields.Nested(AufAbschlagSchema))
+    preisgarantie = fields.Nested(PreisgarantieSchema)
+    tarifeinschraenkung = fields.Nested(TarifeinschraenkungSchema)
diff --git a/src/bo4e/com/tarifberechnungsparameter.py b/src/bo4e/com/tarifberechnungsparameter.py
index 77c96bc42..efe656e33 100644
--- a/src/bo4e/com/tarifberechnungsparameter.py
+++ b/src/bo4e/com/tarifberechnungsparameter.py
@@ -91,13 +91,13 @@ class TarifberechnungsparameterSchema(COMSchema):
 
     class_name = Tarifberechnungsparameter
     # optional attributes
-    berechnungsmethode = EnumField(Tarifkalkulationsmethode)
-    messpreis_in_gp_enthalten = fields.Bool()
-    messpreis_beruecksichtigen = fields.Bool()
-    messpreistyp = EnumField(Messpreistyp)
-    kw_inklusive = fields.Decimal(as_string=True)
-    kw_weitere_mengen = fields.Decimal(as_string=True)
-    hoechstpreis_n_t = fields.Nested(PreisSchema, data_key="hoechstpreisHT")
-    hoechstpreis_h_t = fields.Nested(PreisSchema, data_key="hoechstpreisNT")
-    mindestpreis = fields.Nested(PreisSchema)
-    zusatzpreise = fields.List(fields.Nested(TarifpreisSchema))
+    berechnungsmethode = EnumField(Tarifkalkulationsmethode, allow_none=True)
+    messpreis_in_gp_enthalten = fields.Bool(allow_none=True)
+    messpreis_beruecksichtigen = fields.Bool(allow_none=True)
+    messpreistyp = EnumField(Messpreistyp, allow_none=True)
+    kw_inklusive = fields.Decimal(as_string=True, allow_none=True)
+    kw_weitere_mengen = fields.Decimal(as_string=True, allow_none=True)
+    hoechstpreis_n_t = fields.Nested(PreisSchema, data_key="hoechstpreisHT", allow_none=True)
+    hoechstpreis_h_t = fields.Nested(PreisSchema, data_key="hoechstpreisNT", allow_none=True)
+    mindestpreis = fields.Nested(PreisSchema, allow_none=True)
+    zusatzpreise = fields.List(fields.Nested(TarifpreisSchema), allow_none=True)


Test patch:
diff --git a/tests/test_aufabschlag.py b/tests/test_aufabschlag.py
index bfeb54702..6261c701b 100644
--- a/tests/test_aufabschlag.py
+++ b/tests/test_aufabschlag.py
@@ -4,15 +4,25 @@
 import pytest  # type:ignore[import]
 
 from bo4e.com.aufabschlag import AufAbschlag, AufAbschlagSchema
-from bo4e.com.com import COM, COMSchema
-from bo4e.com.preisstaffel import Preisstaffel, PreisstaffelSchema
-from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
+from bo4e.com.preisstaffel import Preisstaffel
+from bo4e.com.zeitraum import Zeitraum
 from bo4e.enum.aufabschlagstyp import AufAbschlagstyp
 from bo4e.enum.aufabschlagsziel import AufAbschlagsziel
 from bo4e.enum.waehrungseinheit import Waehrungseinheit
 from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
 from tests.test_sigmoidparameter import example_sigmoidparameter  # type:ignore[import]
 
+example_aufabschlag = AufAbschlag(
+    bezeichnung="foo",
+    staffeln=[
+        Preisstaffel(
+            einheitspreis=Decimal(15.0),
+            staffelgrenze_von=Decimal(2.5),
+            staffelgrenze_bis=Decimal(40.5),
+        ),
+    ],
+)
+
 
 class TestAufAbschlag:
     @pytest.mark.parametrize(
@@ -77,16 +87,7 @@ class TestAufAbschlag:
                 id="maximal attributes",
             ),
             pytest.param(
-                AufAbschlag(
-                    bezeichnung="foo",
-                    staffeln=[
-                        Preisstaffel(
-                            einheitspreis=Decimal(15.0),
-                            staffelgrenze_von=Decimal(2.5),
-                            staffelgrenze_bis=Decimal(40.5),
-                        ),
-                    ],
-                ),
+                example_aufabschlag,
                 {
                     "bezeichnung": "foo",
                     "beschreibung": None,
diff --git a/tests/test_tarifberechnungsparameter.py b/tests/test_tarifberechnungsparameter.py
index c5ab04a71..e42c1ae69 100644
--- a/tests/test_tarifberechnungsparameter.py
+++ b/tests/test_tarifberechnungsparameter.py
@@ -32,6 +32,10 @@ class TestFremdkostenposition:
                 example_tarifberechnungsparameter,
                 id="maximal attributes",
             ),
+            pytest.param(
+                Tarifberechnungsparameter(),
+                id="minimal attributes",
+            ),
         ],
     )
     def test_serialization_roundtrip(self, tarifberechnungsparameter: Fremdkostenposition):
diff --git a/tests/test_tarifpreisblatt.py b/tests/test_tarifpreisblatt.py
new file mode 100644
index 000000000..c75b221cd
--- /dev/null
+++ b/tests/test_tarifpreisblatt.py
@@ -0,0 +1,65 @@
+import datetime
+
+import pytest  # type:ignore[import]
+
+from bo4e.bo.tarifpreisblatt import Tarifpreisblatt, TarifpreisblattSchema
+from bo4e.com.tarifberechnungsparameter import Tarifberechnungsparameter
+from bo4e.com.tarifeinschraenkung import Tarifeinschraenkung
+from bo4e.enum.kundentyp import Kundentyp
+from bo4e.enum.sparte import Sparte
+from bo4e.enum.tarifart import Tarifart
+from bo4e.enum.tarifmerkmal import Tarifmerkmal
+from bo4e.enum.tariftyp import Tariftyp
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+from tests.test_aufabschlag import example_aufabschlag  # type:ignore[import]
+from tests.test_energiemix import example_energiemix  # 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_preisgarantie import example_preisgarantie  # type:ignore[import]
+from tests.test_preisposition import example_preisposition  # type:ignore[import]
+from tests.test_tarifpreisposition import example_tarifpreisposition  # type:ignore[import]
+from tests.test_vertragskonditionen import example_vertragskonditionen  # type:ignore[import]
+from tests.test_zeitraum import example_zeitraum  # type:ignore[import]
+
+
+class TestTarifpreisblatt:
+    @pytest.mark.parametrize(
+        "tarifpreisblatt",
+        [
+            pytest.param(
+                Tarifpreisblatt(
+                    bezeichnung="foo",
+                    anbietername="der beste stromanbieter",
+                    sparte=Sparte.STROM,
+                    kundentypen=[Kundentyp.PRIVAT, Kundentyp.GEWERBE],
+                    tarifart=Tarifart.MEHRTARIF,
+                    tariftyp=Tariftyp.GRUND_ERSATZVERSORGUNG,
+                    tarifmerkmale=[Tarifmerkmal.HEIZSTROM],
+                    website="https://foo.inv",
+                    bemerkung="super billig aber auch super dreckig",
+                    vertragskonditionen=example_vertragskonditionen,
+                    zeitliche_gueltigkeit=example_zeitraum,
+                    energiemix=example_energiemix,
+                    anbieter=example_marktteilnehmer,
+                    # ^^ above is all copy pasted from Tarifinfo BO
+                    # vv below are the attributes of tarifpreisblatt
+                    preisstand=datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
+                    berechnungsparameter=Tarifberechnungsparameter(),
+                    tarif_auf_abschlaege=[example_aufabschlag],
+                    tarifpreise=[example_tarifpreisposition],
+                    preisgarantie=example_preisgarantie,
+                    tarifeinschraenkung=Tarifeinschraenkung(),
+                )
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, tarifpreisblatt: Tarifpreisblatt):
+        """
+        Test de-/serialisation
+        """
+        assert_serialization_roundtrip(tarifpreisblatt, TarifpreisblattSchema())
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Tarifpreisblatt()
+        assert "missing 11 required" in str(excinfo.value)  # 8 from tarifinfo + 3 from tarifpreisblatt
diff --git a/tests/test_tarifpreisposition.py b/tests/test_tarifpreisposition.py
index 901d7290a..f41fb9741 100644
--- a/tests/test_tarifpreisposition.py
+++ b/tests/test_tarifpreisposition.py
@@ -9,24 +9,26 @@
 from bo4e.enum.waehrungseinheit import Waehrungseinheit
 from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
 
+example_tarifpreisposition = Tarifpreisposition(
+    preistyp=Preistyp.ENTGELT_ABLESUNG,
+    einheit=Waehrungseinheit.EUR,
+    bezugseinheit=Mengeneinheit.KWH,
+    preisstaffeln=[
+        Preisstaffel(
+            einheitspreis=Decimal(40.0),
+            staffelgrenze_von=Decimal(12.5),
+            staffelgrenze_bis=Decimal(25.0),
+        ),
+    ],
+)
+
 
 class TestTarifpreisposition:
     @pytest.mark.parametrize(
         "tarifpreisposition, expected_json_dict",
         [
             pytest.param(
-                Tarifpreisposition(
-                    preistyp=Preistyp.ENTGELT_ABLESUNG,
-                    einheit=Waehrungseinheit.EUR,
-                    bezugseinheit=Mengeneinheit.KWH,
-                    preisstaffeln=[
-                        Preisstaffel(
-                            einheitspreis=Decimal(40.0),
-                            staffelgrenze_von=Decimal(12.5),
-                            staffelgrenze_bis=Decimal(25.0),
-                        ),
-                    ],
-                ),
+                example_tarifpreisposition,
                 {
                     "preistyp": "ENTGELT_ABLESUNG",
                     "einheit": "EUR",