diff --git a/src/bo4e/bo/tarif.py b/src/bo4e/bo/tarif.py new file mode 100644 index 000000000..1bb523d4b --- /dev/null +++ b/src/bo4e/bo/tarif.py @@ -0,0 +1,89 @@ +""" +Contains Tarif 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.aufabschlagregional import AufAbschlagRegional, AufAbschlagRegionalSchema +from bo4e.com.preisgarantie import Preisgarantie, PreisgarantieSchema +from bo4e.com.tarifberechnungsparameter import Tarifberechnungsparameter, TarifberechnungsparameterSchema +from bo4e.com.tarifeinschraenkung import Tarifeinschraenkung, TarifeinschraenkungSchema +from bo4e.com.tarifpreispositionproort import TarifpreispositionProOrt, TarifpreispositionProOrtSchema +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 Tarif(Tarifinfo): + """ + Abbildung eines Tarifs mit regionaler Zuordnung von Preisen und Auf- und Abschlägen + """ + + bo_typ: BoTyp = attr.ib(default=BoTyp.TARIF) + # required attributes + #: Gibt an, wann der Preis zuletzt angepasst wurde + preisstand: datetime = attr.ib(validator=attr.validators.instance_of(datetime)) + #: Für die Berechnung der Kosten sind die hier abgebildeten Parameter heranzuziehen + berechnungsparameter: Tarifberechnungsparameter = attr.ib( + validator=attr.validators.instance_of(Tarifberechnungsparameter) + ) + #: Die festgelegten Preise mit regionaler Eingrenzung z.B. für Arbeitspreis, Grundpreis etc. + tarifpreise: List[TarifpreispositionProOrt] = attr.ib( + validator=attr.validators.deep_iterable( + member_validator=attr.validators.instance_of(TarifpreispositionProOrt), + iterable_validator=check_list_length_at_least_one, + ) + ) + + # optional attributes + #: Auf- und Abschläge auf die Preise oder Kosten mit regionaler Eingrenzung + tarif_auf_abschlaege: Optional[List[AufAbschlagRegional]] = attr.ib( + default=None, + validator=attr.validators.optional( + attr.validators.deep_iterable( + member_validator=attr.validators.instance_of(AufAbschlagRegional), + iterable_validator=attr.validators.instance_of(list), + ) + ), + ) + # todo: fix inconsistency: RegionalerAufAbschlag vs. AufAbschlagRegional + # https://github.com/Hochfrequenz/BO4E-python/issues/345 + + #: Preisgarantie für diesen Tarif + preisgarantie: Optional[Preisgarantie] = attr.ib( + default=None, + validator=attr.validators.optional( + attr.validators.instance_of(Preisgarantie), + ), + ) + # todo: fix inconsistency with regionaltarif https://github.com/Hochfrequenz/BO4E-python/issues/346 + #: 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)) + ) + + +class TarifSchema(TarifinfoSchema): + """ + Schema for de-/serialization of Tarif + """ + + class_name = Tarif # type:ignore[assignment] + + # required attributes + preisstand = fields.DateTime() + berechnungsparameter = fields.Nested(TarifberechnungsparameterSchema) + tarifpreise = fields.List(fields.Nested(TarifpreispositionProOrtSchema)) + + # optional attributes + tarif_auf_abschlaege = fields.List( + fields.Nested(AufAbschlagRegionalSchema), allow_none=True, data_key="tarifAufAbschlaege" + ) + preisgarantie = fields.Nested(PreisgarantieSchema, allow_none=True) + tarifeinschraenkung = fields.Nested(TarifeinschraenkungSchema, allow_none=True) diff --git a/src/bo4e/com/aufabschlagstaffelproort.py b/src/bo4e/com/aufabschlagstaffelproort.py index e68ff15d6..2cb35ac00 100644 --- a/src/bo4e/com/aufabschlagstaffelproort.py +++ b/src/bo4e/com/aufabschlagstaffelproort.py @@ -15,7 +15,7 @@ @attr.s(auto_attribs=True, kw_only=True) class AufAbschlagstaffelProOrt(COM): """ - Gibt den Wert eines Auf- oder Abschlags und dessen Staffelgrenzen an. + Gibt den Wert eines Auf- oder Abschlags und dessen Staffelgrenzen an """ # required attributes @@ -29,7 +29,7 @@ class AufAbschlagstaffelProOrt(COM): class AufAbschlagstaffelProOrtSchema(COMSchema): """ - Schema for de-/serialization of AufAbschlagstaffelProOrt. + Schema for de-/serialization of AufAbschlagstaffelProOrt """ class_name = AufAbschlagstaffelProOrt diff --git a/src/bo4e/com/tarifpreisposition.py b/src/bo4e/com/tarifpreisposition.py index 232baf8eb..fd21aea3b 100644 --- a/src/bo4e/com/tarifpreisposition.py +++ b/src/bo4e/com/tarifpreisposition.py @@ -25,13 +25,13 @@ class Tarifpreisposition(COM): """ # required attributes - # Angabe des Preistypes (z.B. Grundpreis) + #: Angabe des Preistypes (z.B. Grundpreis) preistyp: Preistyp = attr.ib(validator=attr.validators.instance_of(Preistyp)) - # Einheit des Preises (z.B. EURO) + #: Einheit des Preises (z.B. EURO) einheit: Waehrungseinheit = attr.ib(validator=attr.validators.instance_of(Waehrungseinheit)) - # Größe, auf die sich die Einheit bezieht, beispielsweise kWh, Jahr + #: Größe, auf die sich die Einheit bezieht, beispielsweise kWh, Jahr bezugseinheit: Mengeneinheit = attr.ib(validator=attr.validators.instance_of(Mengeneinheit)) - # Hier sind die Staffeln mit ihren Preisenangaben definiert + #: Hier sind die Staffeln mit ihren Preisenangaben definiert preisstaffeln: List[Preisstaffel] = attr.ib( validator=[ attr.validators.deep_iterable( @@ -43,7 +43,7 @@ class Tarifpreisposition(COM): ) # optional attributes - # Gibt an, nach welcher Menge die vorgenannte Einschränkung erfolgt (z.B. Jahresstromverbrauch in kWh) + #: Gibt an, nach welcher Menge die vorgenannte Einschränkung erfolgt (z.B. Jahresstromverbrauch in kWh) mengeneinheitstaffel: Optional[Mengeneinheit] = attr.ib( default=None, validator=attr.validators.optional(attr.validators.instance_of(Mengeneinheit)) ) diff --git a/src/bo4e/com/tarifpreispositionproort.py b/src/bo4e/com/tarifpreispositionproort.py new file mode 100644 index 000000000..d253e13b9 --- /dev/null +++ b/src/bo4e/com/tarifpreispositionproort.py @@ -0,0 +1,52 @@ +""" +Contains TarifpreispositionProOrt class +and corresponding marshmallow schema for de-/serialization +""" + +from typing import List + +import attr +from marshmallow import fields + +from bo4e.com.com import COM, COMSchema +from bo4e.com.tarifpreisstaffelproort import TarifpreisstaffelProOrt, TarifpreisstaffelProOrtSchema +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 TarifpreispositionProOrt(COM): + """ + Mit dieser Komponente können Tarifpreise verschiedener Typen abgebildet werden + """ + + # required attributes + #: Postleitzahl des Ortes für den der Preis gilt + postleitzahl: str = attr.ib(validator=attr.validators.matches_re(r"^\d{5}$")) + #: Ort für den der Preis gilt + ort: str = attr.ib(validator=attr.validators.instance_of(str)) + #: ene't-Netznummer des Netzes in dem der Preis gilt + netznr: str = attr.ib(validator=attr.validators.instance_of(str)) + # Hier sind die Staffeln mit ihren Preisenangaben definiert + preisstaffeln: List[TarifpreisstaffelProOrt] = attr.ib( + validator=[ + attr.validators.deep_iterable( + member_validator=attr.validators.instance_of(TarifpreisstaffelProOrt), + iterable_validator=check_list_length_at_least_one, + ), + ] + ) + # there are no optional attributes + + +class TarifpreispositionProOrtSchema(COMSchema): + """ + Schema for de-/serialization of TarifpreispositionProOrt. + """ + + class_name = TarifpreispositionProOrt + # required attributes + postleitzahl = fields.Str() + ort = fields.Str() + netznr = fields.Str() + preisstaffeln = fields.List(fields.Nested(TarifpreisstaffelProOrtSchema)) diff --git a/src/bo4e/com/tarifpreisstaffelproort.py b/src/bo4e/com/tarifpreisstaffelproort.py new file mode 100644 index 000000000..2114b0159 --- /dev/null +++ b/src/bo4e/com/tarifpreisstaffelproort.py @@ -0,0 +1,50 @@ +""" +Contains TarifpreisstaffelProOrt class +and corresponding marshmallow schema for de-/serialization +""" +from decimal import Decimal + +import attr +from marshmallow import fields + +from bo4e.com.com import COM, COMSchema + + +# pylint: disable=too-few-public-methods +@attr.s(auto_attribs=True, kw_only=True) +class TarifpreisstaffelProOrt(COM): + """ + Gibt die Staffelgrenzen der jeweiligen Preise an + """ + + # todo: decimal doesn't make sense here imo + # https://github.com/Hochfrequenz/BO4E-python/issues/344 + + # required attributes + #: Der Arbeitspreis in ct/kWh + arbeitspreis: Decimal = attr.ib(validator=attr.validators.instance_of(Decimal)) + #: Der Arbeitspreis für Verbräuche in der Niedertarifzeit in ct/kWh + arbeitspreis_n_t: Decimal = attr.ib(validator=attr.validators.instance_of(Decimal)) + #: Der Grundpreis in Euro/Jahr + grundpreis: Decimal = attr.ib(validator=attr.validators.instance_of(Decimal)) + #: Unterer Wert, ab dem die Staffel gilt (inklusive) + staffelgrenze_von: Decimal = attr.ib(validator=attr.validators.instance_of(Decimal)) + #: Oberer Wert, bis zu dem die Staffel gilt (exklusive) + staffelgrenze_bis: Decimal = attr.ib(validator=attr.validators.instance_of(Decimal)) + + # there are no optional attributes + + +class TarifpreisstaffelProOrtSchema(COMSchema): + """ + Schema for (de)serialization of TarifpreisstaffelProOrt + """ + + class_name = TarifpreisstaffelProOrt + + # required attributes + arbeitspreis = fields.Decimal(as_string=True) + arbeitspreis_n_t = fields.Decimal(as_string=True, data_key="arbeitspreisNT") + grundpreis = fields.Decimal(as_string=True) + staffelgrenze_von = fields.Decimal(as_string=True, data_key="staffelgrenzeVon") + staffelgrenze_bis = fields.Decimal(as_string=True, data_key="staffelgrenzeBis") diff --git a/src/bo4e/enum/botyp.py b/src/bo4e/enum/botyp.py index 84def944e..4b83d4de8 100644 --- a/src/bo4e/enum/botyp.py +++ b/src/bo4e/enum/botyp.py @@ -36,6 +36,7 @@ class BoTyp(StrEnum): REGION = "REGION" REGIONALTARIF = "REGIONALTARIF" STANDORTEIGENSCHAFTEN = "STANDORTEIGENSCHAFTEN" + TARIF = "TARIF" TARIFINFO = "TARIFINFO" TARIFKOSTEN = "TARIFKOSTEN" TARIFPREISBLATT = "TARIFPREISBLATT" diff --git a/tests/test_aufabschlagregional.py b/tests/test_aufabschlagregional.py index 58974f3e1..51482ecdf 100644 --- a/tests/test_aufabschlagregional.py +++ b/tests/test_aufabschlagregional.py @@ -20,29 +20,31 @@ from bo4e.enum.waehrungseinheit import Waehrungseinheit from tests.serialization_helper import assert_serialization_roundtrip # type:ignore[import] +example_aufabschlagregional = AufAbschlagRegional( + bezeichnung="foo", + betraege=[ + AufAbschlagProOrt( + postleitzahl="01187", + ort="Dresden", + netznr="2", + staffeln=[ + AufAbschlagstaffelProOrt( + wert=Decimal(2.5), + staffelgrenze_von=Decimal(1), + staffelgrenze_bis=Decimal(5), + ) + ], + ), + ], +) + class TestAufAbschlagRegional: @pytest.mark.parametrize( "aufabschlagregional, expected_json_dict", [ pytest.param( - AufAbschlagRegional( - bezeichnung="foo", - betraege=[ - AufAbschlagProOrt( - postleitzahl="01187", - ort="Dresden", - netznr="2", - staffeln=[ - AufAbschlagstaffelProOrt( - wert=Decimal(2.5), - staffelgrenze_von=Decimal(1), - staffelgrenze_bis=Decimal(5), - ) - ], - ), - ], - ), + example_aufabschlagregional, { "bezeichnung": "foo", "betraege": [ diff --git a/tests/test_tarif.py b/tests/test_tarif.py new file mode 100644 index 000000000..d6368cf7b --- /dev/null +++ b/tests/test_tarif.py @@ -0,0 +1,81 @@ +from datetime import datetime, timezone + +import pytest # type:ignore[import] + +from bo4e.bo.tarif import Tarif, TarifSchema +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_aufabschlagregional import example_aufabschlagregional # type:ignore[import] +from tests.test_energiemix import example_energiemix # 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_regionaletarifpreisposition import example_regionale_tarifpreisposition # type:ignore[import] +from tests.test_tarifberechnungsparameter import example_tarifberechnungsparameter # type:ignore[import] +from tests.test_tarifeinschraenkung import example_tarifeinschraenkung # type:ignore[import] +from tests.test_tarifpreispositionproort import example_tarifpreispositionproort # type:ignore[import] +from tests.test_vertragskonditionen import example_vertragskonditionen # type:ignore[import] +from tests.test_zeitraum import example_zeitraum # type:ignore[import] + + +class TestTarif: + @pytest.mark.parametrize( + "tarif", + [ + pytest.param( + Tarif( + preisstand=datetime(2022, 2, 1, 0, 0, 0, tzinfo=timezone.utc), + berechnungsparameter=example_tarifberechnungsparameter, + tarif_auf_abschlaege=[example_aufabschlagregional], + tarifpreise=[example_tarifpreispositionproort], + preisgarantie=example_preisgarantie, + tarifeinschraenkung=example_tarifeinschraenkung, + # below are the attributes of tarifinfo + 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, + ), + id="required and optional attributes", + ), + pytest.param( + Tarif( + preisstand=datetime(2022, 2, 1, 0, 0, 0, tzinfo=timezone.utc), + berechnungsparameter=example_tarifberechnungsparameter, + tarifpreise=[example_tarifpreispositionproort], + # below are the attributes of tarifinfo + bezeichnung="foo", + anbietername="der beste stromanbieter", + sparte=Sparte.STROM, + kundentypen=[Kundentyp.PRIVAT, Kundentyp.GEWERBE], + tarifart=Tarifart.MEHRTARIF, + tariftyp=Tariftyp.GRUND_ERSATZVERSORGUNG, + tarifmerkmale=[Tarifmerkmal.HEIZSTROM], + anbieter=example_marktteilnehmer, + ), + id="only required attributes", + ), + ], + ) + def test_serialization_roundtrip(self, tarif: Tarif): + """ + Test de-/serialisation + """ + assert_serialization_roundtrip(tarif, TarifSchema()) + + def test_missing_required_attribute(self): + with pytest.raises(TypeError) as excinfo: + _ = Tarif() + assert "missing 11 required" in str(excinfo.value) # 3 from Tarif + 8 from tarifinfo diff --git a/tests/test_tarifpreispositionproort.py b/tests/test_tarifpreispositionproort.py new file mode 100644 index 000000000..0ba93a18c --- /dev/null +++ b/tests/test_tarifpreispositionproort.py @@ -0,0 +1,35 @@ +import pytest # type:ignore[import] + +from bo4e.com.tarifpreispositionproort import TarifpreispositionProOrt, TarifpreispositionProOrtSchema +from tests.serialization_helper import assert_serialization_roundtrip # type:ignore[import] +from tests.test_tarifpreisstaffelproort import example_tarifpreisstaffelproort # type:ignore[import] + +example_tarifpreispositionproort = TarifpreispositionProOrt( + postleitzahl="82031", + ort="Grünwald", + netznr="0815", + preisstaffeln=[example_tarifpreisstaffelproort], +) + + +class TestTarifpreispositionProOrt: + @pytest.mark.parametrize( + "tarifpreispositionproort", + [ + pytest.param( + example_tarifpreispositionproort, + id="minimal and maximal attributes", + ), + ], + ) + def test_serialization_roundtrip(self, tarifpreispositionproort: TarifpreispositionProOrt): + """ + Test de-/serialisation + """ + assert_serialization_roundtrip(tarifpreispositionproort, TarifpreispositionProOrtSchema()) + + def test_missing_required_attribute(self): + with pytest.raises(TypeError) as excinfo: + _ = TarifpreispositionProOrt() + + assert "missing 4 required" in str(excinfo.value) diff --git a/tests/test_tarifpreisstaffelproort.py b/tests/test_tarifpreisstaffelproort.py new file mode 100644 index 000000000..f5fc787aa --- /dev/null +++ b/tests/test_tarifpreisstaffelproort.py @@ -0,0 +1,37 @@ +from decimal import Decimal + +import pytest # type:ignore[import] + +from bo4e.com.tarifpreisstaffelproort import TarifpreisstaffelProOrt, TarifpreisstaffelProOrtSchema +from tests.serialization_helper import assert_serialization_roundtrip # type:ignore[import] + +example_tarifpreisstaffelproort = TarifpreisstaffelProOrt( + arbeitspreis=Decimal(10), + arbeitspreis_n_t=Decimal(11), + grundpreis=Decimal(12), + staffelgrenze_von=Decimal(13), + staffelgrenze_bis=Decimal(14), +) + + +class TestTarifpreisstaffelProOrt: + @pytest.mark.parametrize( + "tarifpreisstaffelproort", + [ + pytest.param( + example_tarifpreisstaffelproort, + id="maximal (and minimal) attributes", + ), + ], + ) + def test_serialization_roundtrip(self, tarifpreisstaffelproort: TarifpreisstaffelProOrt): + """ + Test de-/serialisation + """ + assert_serialization_roundtrip(tarifpreisstaffelproort, TarifpreisstaffelProOrtSchema()) + + def test_missing_required_attribute(self): + with pytest.raises(TypeError) as excinfo: + _ = TarifpreisstaffelProOrt() + + assert "missing 5 required" in str(excinfo.value)