From f3f37709202a1e9357e842653ec864b6f98ff8b9 Mon Sep 17 00:00:00 2001 From: Leon Haffmans Date: Tue, 7 Jul 2026 14:33:46 +0000 Subject: [PATCH 1/3] feat: add Produkttyp enum, Kundentyp.INDUSTRIE, BoTyp.PRODUKT, ComTyp.TARIF --- src/bo4e/__init__.py | 2 ++ src/bo4e/enum/botyp.py | 1 + src/bo4e/enum/comtyp.py | 1 + src/bo4e/enum/kundentyp.py | 2 ++ src/bo4e/enum/produkttyp.py | 11 +++++++++++ 5 files changed, 17 insertions(+) create mode 100644 src/bo4e/enum/produkttyp.py diff --git a/src/bo4e/__init__.py b/src/bo4e/__init__.py index a225fe6d4..bc2a7b127 100644 --- a/src/bo4e/__init__.py +++ b/src/bo4e/__init__.py @@ -163,6 +163,7 @@ "Profilart", "Profiltyp", "Profilverfahren", + "Produkttyp", "Prognosegrundlage", "Rechnungslegung", "Rechnungsstatus", @@ -367,6 +368,7 @@ from .enum.preisreferenz import Preisreferenz from .enum.preisstatus import Preisstatus from .enum.preistyp import Preistyp +from .enum.produkttyp import Produkttyp from .enum.profilart import Profilart from .enum.profiltyp import Profiltyp from .enum.profilverfahren import Profilverfahren diff --git a/src/bo4e/enum/botyp.py b/src/bo4e/enum/botyp.py index 796274b3f..1361dd3f8 100644 --- a/src/bo4e/enum/botyp.py +++ b/src/bo4e/enum/botyp.py @@ -40,6 +40,7 @@ class BoTyp(StrEnum): PREISBLATTMESSUNG = "PREISBLATTMESSUNG" PREISBLATTNETZNUTZUNG = "PREISBLATTNETZNUTZUNG" PREISBLATTUMLAGEN = "PREISBLATTUMLAGEN" + PRODUKT = "PRODUKT" RECHNUNG = "RECHNUNG" REGION = "REGION" STANDORTEIGENSCHAFTEN = "STANDORTEIGENSCHAFTEN" diff --git a/src/bo4e/enum/comtyp.py b/src/bo4e/enum/comtyp.py index 29abed106..71de53fec 100644 --- a/src/bo4e/enum/comtyp.py +++ b/src/bo4e/enum/comtyp.py @@ -51,6 +51,7 @@ class ComTyp(StrEnum): STANDORTEIGENSCHAFTENSTROM = "STANDORTEIGENSCHAFTENSTROM" STEUERBETRAG = "STEUERBETRAG" TAGESPARAMETER = "TAGESPARAMETER" + TARIF = "TARIF" TARIFBERECHNUNGSPARAMETER = "TARIFBERECHNUNGSPARAMETER" TARIFEINSCHRAENKUNG = "TARIFEINSCHRAENKUNG" TARIFPREIS = "TARIFPREIS" diff --git a/src/bo4e/enum/kundentyp.py b/src/bo4e/enum/kundentyp.py index cb6ec8986..df33c0833 100644 --- a/src/bo4e/enum/kundentyp.py +++ b/src/bo4e/enum/kundentyp.py @@ -37,3 +37,5 @@ class Kundentyp(StrEnum): """Unterbrechbare Verbrauchseinrichtungen""" WAERMEPUMPE = "WAERMEPUMPE" """Wärmepumpen""" + INDUSTRIE = "INDUSTRIE" + """Großkunden / Kunden, die viel Energie verbrauchen""" diff --git a/src/bo4e/enum/produkttyp.py b/src/bo4e/enum/produkttyp.py new file mode 100644 index 000000000..6f0ea3520 --- /dev/null +++ b/src/bo4e/enum/produkttyp.py @@ -0,0 +1,11 @@ +# pylint:disable=missing-module-docstring +from bo4e.enum.strenum import StrEnum + + +class Produkttyp(StrEnum): + """ + Aufzählung der Produktarten. + """ + + TARIFPRODUKT = "TARIFPRODUKT" + """Ein Produkt, das auf einem Tarif basiert.""" From ab13883051451b0a613abbebb7000402e451b16a Mon Sep 17 00:00:00 2001 From: Leon Haffmans Date: Tue, 7 Jul 2026 14:42:55 +0000 Subject: [PATCH 2/3] refactor: degrade BO Tarif to COM Tarif, promote-away identifying fields --- src/bo4e/__init__.py | 2 +- src/bo4e/{bo => com}/tarif.py | 61 ++++++++--------------------------- src/bo4e/enum/botyp.py | 1 - tests/test_tarif.py | 19 +++-------- 4 files changed, 20 insertions(+), 63 deletions(-) rename src/bo4e/{bo => com}/tarif.py (52%) diff --git a/src/bo4e/__init__.py b/src/bo4e/__init__.py index bc2a7b127..8d0860143 100644 --- a/src/bo4e/__init__.py +++ b/src/bo4e/__init__.py @@ -238,7 +238,6 @@ from .bo.region import Region from .bo.standorteigenschaften import Standorteigenschaften from .bo.steuerbareressource import SteuerbareRessource -from .bo.tarif import Tarif from .bo.tarifinfo import Tarifinfo from .bo.tarifkosten import Tarifkosten from .bo.tarifpreisblatt import Tarifpreisblatt @@ -290,6 +289,7 @@ from .com.standorteigenschaftenstrom import StandorteigenschaftenStrom from .com.steuerbetrag import Steuerbetrag from .com.tagesparameter import Tagesparameter +from .com.tarif import Tarif from .com.tarifberechnungsparameter import Tarifberechnungsparameter from .com.tarifeinschraenkung import Tarifeinschraenkung from .com.tarifpreis import Tarifpreis diff --git a/src/bo4e/bo/tarif.py b/src/bo4e/com/tarif.py similarity index 52% rename from src/bo4e/bo/tarif.py rename to src/bo4e/com/tarif.py index 51e4d44e3..0bfc3dfb5 100644 --- a/src/bo4e/bo/tarif.py +++ b/src/bo4e/com/tarif.py @@ -6,34 +6,28 @@ from pydantic import Field -from ..enum.botyp import BoTyp +from ..enum.comtyp import ComTyp from ..utils import postprocess_docstring -from .tarifinfo import Tarifinfo +from .com import COM if TYPE_CHECKING: - from ..bo.marktteilnehmer import Marktteilnehmer from ..com.energiemix import Energiemix from ..com.preisgarantie import Preisgarantie from ..com.regionspreis import Regionspreis from ..com.tarifberechnungsparameter import Tarifberechnungsparameter from ..com.tarifeinschraenkung import Tarifeinschraenkung - from ..com.vertragskonditionen import Vertragskonditionen - from ..com.zeitraum import Zeitraum - from ..enum.kundentyp import Kundentyp from ..enum.registeranzahl import Registeranzahl - from ..enum.sparte import Sparte from ..enum.tarifmerkmal import Tarifmerkmal from ..enum.tariftyp import Tariftyp - # pylint: disable=too-few-public-methods # pylint: disable=no-name-in-module @postprocess_docstring -class Tarif(Tarifinfo): +class Tarif(COM): """ - Abbildung eines Tarifs. + Abbildung eines Tarifs (Preisbestandteile eines Produkts). Der Tarifpreis kann regionsaufgelöst und unter Angabe von Zeitscheiben angegeben werden. So kann bspw. derselbe Tarif je nach Region andere Preise aufweisen. Es können auch Tarifpreise abgebildet werden, die sich ab einem @@ -43,25 +37,18 @@ class Tarif(Tarifinfo): `COM RelativePreisposition` prozentuale Auf- und Abschläge auf andere Preispositionen definiert werden. Alle Preispositionen hängen unter `COM Tarifpreiszeitscheibe` mit einer Ausnahme. - Möchten Sie einen dynamischen Tarif modellieren, so gibt es das `COM DynamischePreisposition`. Da diese - Preisposition weder orts- noch zeitabhängig ist, hängt diese direkt unter dem `BO Tarif`. Eine zeitabhängige - Änderung einer dynamischen Tarifpreisposition ist unsinnig, da es sich (unserer Ansicht nach) dann um einen - völlig neuen Tarif handelt. Davon unabhängig können (und müssen) natürlich weiterhin zusätzlich alle anderen - Preispositionen orts- und zeitabhängig angegeben werden. - - > Hinweis: Das Vorhandensein einer `COM DynamischePreisposition` dient gleichzeitig auch als "Flag" dafür, ob - > es sich bei diesem Tarif um einen dynamischen handelt. + Möchten Sie einen dynamischen Tarif modellieren, so gibt es das `COM DynamischePreisposition`. .. raw:: html - + .. HINT:: - `Tarif JSON Schema `_ + `Tarif JSON Schema `_ """ - typ: Annotated[Literal[BoTyp.TARIF], Field(alias="_typ")] = BoTyp.TARIF # type:ignore[assignment] + typ: Annotated[Literal[ComTyp.TARIF], Field(alias="_typ")] = ComTyp.TARIF regionspreise: Optional[list["Regionspreis"]] = None """ @@ -74,40 +61,20 @@ class Tarif(Tarifinfo): Gibt die Bezugsquelle (z.B. Börsenindex) für den dynamischen Tarif an. Dieses Feld muss genau dann gesetzt werden, wenn es sich bei diesem Tarif um einen dynamischen Tarif handelt. """ - - bezeichnung: Optional[str] = None - """Eine (beliebige) Bezeichnung für den Tarif.""" - beschreibung: Optional[str] = None - """Eine (beliebige) Beschreibung für den Tarif.""" - anbieter: Optional["Marktteilnehmer"] = None - """Der Marktteilnehmer, der diesen Tarif anbietet, angeboten hat oder anbieten wird.""" - - zeitraum_vermarktung: Optional["Zeitraum"] = None - """Der Zeitraum, in dem der Tarif beim Anbieter vertraglich abschließbar ist.""" - zeitraum_belieferbarkeit: Optional["Zeitraum"] = None - """Der Zeitraum, in dem eine Belieferung (für diesen Tarif) möglich ist.""" - vertragskonditionen: Optional["Vertragskonditionen"] - """Vertragskonditionen für diesen Tarif.""" - website: Optional[str] - """Internetseite, auf der der Tarif veröffentlicht ist.""" - energiemix: Optional[list["Energiemix"]] # type:ignore[assignment] + energiemix: Optional[list["Energiemix"]] = None """Der Energiemix mit einem Eintrag pro Gültigkeitsjahr (siehe `Energiemix.gueltigkeitsjahr`).""" - kundentypen: Optional[list["Kundentyp"]] - """Eine Liste an Kundentypen, für die dieser Tarif vorgesehen ist.""" - tariftyp: Optional["Tariftyp"] + tariftyp: Optional["Tariftyp"] = None """Der Tariftyp. Bsp.: Grundversorgung, Ersatzversorgung, etc.""" - tarifmerkmale: Optional[list["Tarifmerkmal"]] + tarifmerkmale: Optional[list["Tarifmerkmal"]] = None """Eine Liste von Produktmerkmalen im Zusammenhang mit diesem Tarif.""" - sparte: Optional["Sparte"] - """Strom / Gas""" - registeranzahl: Optional["Registeranzahl"] + registeranzahl: Optional["Registeranzahl"] = None """ Hinweis zu den Registern bzw. Zählwerken. Bspw. benötigt ein HT-/NT-Tarif auch eine entsprechende Registeranzahl. """ - preisgarantie: Optional["Preisgarantie"] + preisgarantie: Optional["Preisgarantie"] = None """Preisgarantie für diesen Tarif""" - berechnungsparameter: Optional["Tarifberechnungsparameter"] + berechnungsparameter: Optional["Tarifberechnungsparameter"] = None """Für die Berechnung der Kosten sind die hier abgebildeten Parameter heranzuziehen""" tarifeinschraenkung: Optional["Tarifeinschraenkung"] = None """Die Bedingungen und Einschränkungen unter denen ein Tarif angewendet werden kann""" diff --git a/src/bo4e/enum/botyp.py b/src/bo4e/enum/botyp.py index 1361dd3f8..afa1e4261 100644 --- a/src/bo4e/enum/botyp.py +++ b/src/bo4e/enum/botyp.py @@ -44,7 +44,6 @@ class BoTyp(StrEnum): RECHNUNG = "RECHNUNG" REGION = "REGION" STANDORTEIGENSCHAFTEN = "STANDORTEIGENSCHAFTEN" - TARIF = "TARIF" TARIFINFO = "TARIFINFO" TARIFKOSTEN = "TARIFKOSTEN" TARIFPREISBLATT = "TARIFPREISBLATT" diff --git a/tests/test_tarif.py b/tests/test_tarif.py index babf4b407..7d683e79b 100644 --- a/tests/test_tarif.py +++ b/tests/test_tarif.py @@ -4,10 +4,9 @@ import pytest from bo4e import ( + ComTyp, EinheitsPreisposition, Energiemix, - Kundentyp, - Marktteilnehmer, Mengeneinheit, Operator, Preis, @@ -20,14 +19,12 @@ Regionszeitscheibe, Registeranzahl, RelativePreisposition, - Sparte, Tarif, Tarifberechnungsparameter, Tarifeinschraenkung, Tarifmerkmal, Tarifpreiszeitscheibe, Tariftyp, - Vertragskonditionen, Waehrungseinheit, Zeitraum, ) @@ -43,20 +40,10 @@ class TestTarif: berechnungsparameter=Tarifberechnungsparameter(), preisgarantie=Preisgarantie(), tarifeinschraenkung=Tarifeinschraenkung(), - # below are the attributes of tarifinfo - bezeichnung="foo", - anbietername="der beste stromanbieter", - sparte=Sparte.STROM, - kundentypen=[Kundentyp.PRIVAT, Kundentyp.GEWERBE], registeranzahl=Registeranzahl.MEHRTARIF, tariftyp=Tariftyp.GRUND_ERSATZVERSORGUNG, tarifmerkmale=[Tarifmerkmal.HEIZSTROM], - website="https://foo.inv", - bemerkung="super billig aber auch super dreckig", - vertragskonditionen=Vertragskonditionen(), - zeitliche_gueltigkeit=Zeitraum(), energiemix=[Energiemix()], - anbieter=Marktteilnehmer(), regionspreise=[ Regionspreis( regionszeitscheiben=[ @@ -111,3 +98,7 @@ def test_serialization_roundtrip(self, tarif: Tarif) -> None: Test de-/serialisation """ assert_serialization_roundtrip(tarif) + + def test_tarif_typ(self) -> None: + tarif = Tarif() + assert tarif.typ == ComTyp.TARIF From 700bdd244c8bcd90606c592697903ad5d4a7a2c9 Mon Sep 17 00:00:00 2001 From: Leon Haffmans Date: Tue, 7 Jul 2026 14:50:50 +0000 Subject: [PATCH 3/3] feat: add BO Produkt --- src/bo4e/__init__.py | 4 ++- src/bo4e/bo/produkt.py | 70 ++++++++++++++++++++++++++++++++++++++++++ tests/test_produkt.py | 28 +++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/bo4e/bo/produkt.py create mode 100644 tests/test_produkt.py diff --git a/src/bo4e/__init__.py b/src/bo4e/__init__.py index 8d0860143..d198e5d8b 100644 --- a/src/bo4e/__init__.py +++ b/src/bo4e/__init__.py @@ -33,6 +33,7 @@ "PreisblattKonzessionsabgabe", "PreisblattMessung", "PreisblattNetznutzung", + "Produkt", "Rechnung", "Region", "Standorteigenschaften", @@ -160,10 +161,10 @@ "Preisreferenz", "Preisstatus", "Preistyp", + "Produkttyp", "Profilart", "Profiltyp", "Profilverfahren", - "Produkttyp", "Prognosegrundlage", "Rechnungslegung", "Rechnungsstatus", @@ -234,6 +235,7 @@ from .bo.preisblattkonzessionsabgabe import PreisblattKonzessionsabgabe from .bo.preisblattmessung import PreisblattMessung from .bo.preisblattnetznutzung import PreisblattNetznutzung +from .bo.produkt import Produkt from .bo.rechnung import Rechnung from .bo.region import Region from .bo.standorteigenschaften import Standorteigenschaften diff --git a/src/bo4e/bo/produkt.py b/src/bo4e/bo/produkt.py new file mode 100644 index 000000000..8bd6c7f7f --- /dev/null +++ b/src/bo4e/bo/produkt.py @@ -0,0 +1,70 @@ +""" +Contains Produkt class +""" + +from typing import TYPE_CHECKING, Annotated, Literal, Optional + +from pydantic import Field + +from ..enum.botyp import BoTyp +from ..utils import postprocess_docstring +from .geschaeftsobjekt import Geschaeftsobjekt + +if TYPE_CHECKING: + from ..com.tarif import Tarif + from ..com.vertragskonditionen import Vertragskonditionen + from ..com.zeitraum import Zeitraum + from ..enum.kundentyp import Kundentyp + from ..enum.produkttyp import Produkttyp + from ..enum.sparte import Sparte + from .marktteilnehmer import Marktteilnehmer + +# pylint: disable=too-few-public-methods +# pylint: disable=no-name-in-module +# pylint: disable=too-many-instance-attributes + + +@postprocess_docstring +class Produkt(Geschaeftsobjekt): + """ + Modell für die Abbildung eines Produkts, z.B. eines Tarifprodukts. + Ein Produkt bündelt die vermarktungsrelevanten Eigenschaften und referenziert + den zugrundeliegenden Tarif. + + .. raw:: html + + + + .. HINT:: + `Produkt JSON Schema `_ + + """ + + typ: Annotated[Literal[BoTyp.PRODUKT], Field(alias="_typ")] = BoTyp.PRODUKT + + bezeichnung: Optional[str] = None + """Eine (beliebige) Bezeichnung für das Produkt.""" + beschreibung: Optional[str] = None + """Eine (beliebige) Beschreibung für das Produkt.""" + produkttyp: Optional["Produkttyp"] = None + """Die Art des Produkts, z.B. Tarifprodukt.""" + tarif: Optional["Tarif"] = None + """Der dem Produkt zugrundeliegende Tarif.""" + sparte: Optional["Sparte"] = None + """Strom / Gas""" + anbieter: Optional["Marktteilnehmer"] = None + """Der Marktteilnehmer, der dieses Produkt anbietet, angeboten hat oder anbieten wird.""" + zeitraum_vermarktung: Optional["Zeitraum"] = None + """Der Zeitraum, in dem das Produkt beim Anbieter vertraglich abschließbar ist.""" + zeitraum_belieferbarkeit: Optional["Zeitraum"] = None + """Der Zeitraum, in dem eine Belieferung (für dieses Produkt) möglich ist.""" + vertragskonditionen: Optional["Vertragskonditionen"] = None + """ + Vertragskonditionen für dieses Produkt. + Die Redundanz zu den Vertragskonditionen am Vertrag ist gewollt: die Konditionen + des Produkts können bei der Vertragsausgestaltung überschrieben werden. + """ + website: Optional[str] = None + """Internetseite, auf der das Produkt veröffentlicht ist.""" + kundentypen: Optional[list["Kundentyp"]] = None + """Eine Liste an Kundentypen, für die dieses Produkt vorgesehen ist.""" diff --git a/tests/test_produkt.py b/tests/test_produkt.py new file mode 100644 index 000000000..eeaec7add --- /dev/null +++ b/tests/test_produkt.py @@ -0,0 +1,28 @@ +import pytest + +from bo4e import BoTyp, Produkt, Produkttyp, Tarif +from tests.serialization_helper import assert_serialization_roundtrip + + +class TestProdukt: + @pytest.mark.parametrize( + "produkt", + [ + pytest.param( + Produkt( + bezeichnung="Ökostrom Basis", + beschreibung="100% Ökostrom", + produkttyp=Produkttyp.TARIFPRODUKT, + tarif=Tarif(), + website="https://foo.inv", + ), + id="max attributes", + ), + pytest.param(Produkt(), id="min attributes"), + ], + ) + def test_serialization_roundtrip(self, produkt: Produkt) -> None: + assert_serialization_roundtrip(produkt) + + def test_produkt_typ(self) -> None: + assert Produkt().typ == BoTyp.PRODUKT