diff --git a/src/bo4e/__init__.py b/src/bo4e/__init__.py
index a225fe6d4..d198e5d8b 100644
--- a/src/bo4e/__init__.py
+++ b/src/bo4e/__init__.py
@@ -33,6 +33,7 @@
"PreisblattKonzessionsabgabe",
"PreisblattMessung",
"PreisblattNetznutzung",
+ "Produkt",
"Rechnung",
"Region",
"Standorteigenschaften",
@@ -160,6 +161,7 @@
"Preisreferenz",
"Preisstatus",
"Preistyp",
+ "Produkttyp",
"Profilart",
"Profiltyp",
"Profilverfahren",
@@ -233,11 +235,11 @@
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
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
@@ -289,6 +291,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
@@ -367,6 +370,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/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/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 796274b3f..afa1e4261 100644
--- a/src/bo4e/enum/botyp.py
+++ b/src/bo4e/enum/botyp.py
@@ -40,10 +40,10 @@ class BoTyp(StrEnum):
PREISBLATTMESSUNG = "PREISBLATTMESSUNG"
PREISBLATTNETZNUTZUNG = "PREISBLATTNETZNUTZUNG"
PREISBLATTUMLAGEN = "PREISBLATTUMLAGEN"
+ PRODUKT = "PRODUKT"
RECHNUNG = "RECHNUNG"
REGION = "REGION"
STANDORTEIGENSCHAFTEN = "STANDORTEIGENSCHAFTEN"
- TARIF = "TARIF"
TARIFINFO = "TARIFINFO"
TARIFKOSTEN = "TARIFKOSTEN"
TARIFPREISBLATT = "TARIFPREISBLATT"
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."""
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
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