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

Issue:
Implement new BO Buendelvertrag
Hi Leon,
könntest du bitte bei Gelegenheit mal diese Text-Beschreibung des BO Buendelvertrags in eine Klasse gießen: https://www.bo4e.de/dokumentation/geschaeftsobjekte/bo-buendelvertrag/02-03-2022

Das ist eine der letzen Sachen, die noch in #404 fehlt. Wäre cool, wenn du das vllt Ende dieser/Anfang nächster Woche einplanen könntest :) Ist nicht schwierig, nur Fleiß.


Discussion / hints:

Fix patch:
diff --git a/docs/api/bo4e.com.rst b/docs/api/bo4e.com.rst
index 5f5c993c9..7042c3269 100644
--- a/docs/api/bo4e.com.rst
+++ b/docs/api/bo4e.com.rst
@@ -348,14 +348,6 @@ bo4e.com.sigmoidparameter module
    :undoc-members:
    :show-inheritance:
 
-bo4e.com.standorteigenschaftenallgemein module
-----------------------------------------------
-
-.. automodule:: bo4e.com.standorteigenschaftenallgemein
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
 bo4e.com.standorteigenschaftengas module
 ----------------------------------------
 
diff --git a/src/bo4e/bo/buendelvertrag.py b/src/bo4e/bo/buendelvertrag.py
index 22b850603..4ff58ff54 100644
--- a/src/bo4e/bo/buendelvertrag.py
+++ b/src/bo4e/bo/buendelvertrag.py
@@ -4,11 +4,18 @@
 
 # pylint: disable=too-few-public-methods
 # pylint: disable=no-name-in-module
-from pydantic import conlist
+from datetime import datetime
+from typing import List, Optional
 
 from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt
+from bo4e.bo.geschaeftspartner import Geschaeftspartner
 from bo4e.bo.vertrag import Vertrag
+from bo4e.com.unterschrift import Unterschrift
+from bo4e.com.vertragskonditionen import Vertragskonditionen
 from bo4e.enum.botyp import BoTyp
+from bo4e.enum.sparte import Sparte
+from bo4e.enum.vertragsart import Vertragsart
+from bo4e.enum.vertragsstatus import Vertragsstatus
 
 
 class Buendelvertrag(Geschaeftsobjekt):
@@ -27,5 +34,35 @@ class Buendelvertrag(Geschaeftsobjekt):
 
     # required attributes
     bo_typ: BoTyp = BoTyp.BUENDELVERTRAG
+
+    # pylint: disable=duplicate-code
+    #: Eine im Verwendungskontext eindeutige Nummer für den Vertrag
+    vertragsnummer: str
+    #: Hier ist festgelegt, um welche Art von Vertrag es sich handelt. Z.B. Netznutzungvertrag
+    vertragsart: Vertragsart
+    #: Gibt den Status des Vertrages an
+    vertragsstatus: Vertragsstatus
+    #: Unterscheidungsmöglichkeiten für die Sparte
+    sparte: Sparte
+    #: Gibt an, wann der Vertrag beginnt (inklusiv)
+    vertragsbeginn: datetime
+    #: Gibt an, wann der Vertrag (voraussichtlich) endet oder beendet wurde (exklusiv)
+    vertragsende: datetime
+    #: Der "erstgenannte" Vertragspartner. In der Regel der Aussteller des Vertrags.
+    #: Beispiel: "Vertrag zwischen Vertagspartner 1 ..."
+    vertragspartner1: Geschaeftspartner
+    #: Der "zweitgenannte" Vertragspartner. In der Regel der Empfänger des Vertrags.
+    #: Beispiel "Vertrag zwischen Vertagspartner 1 und Vertragspartner 2"
+    vertragspartner2: Geschaeftspartner
+
+    # optional attributes
     #: Die Liste mit den Einzelverträgen zu den Abnahmestellen
-    einzelvertraege: conlist(Vertrag, min_items=1)  # type: ignore[valid-type]
+    einzelvertraege: Optional[List[Vertrag]] = []
+    #: Festlegungen zu Laufzeiten und Kündigungsfristen
+    vertragskonditionen: Optional[List[Vertragskonditionen]] = []
+    #: Unterzeichner des Vertragspartners1
+    unterzeichnervp1: Optional[List[Unterschrift]] = []
+    #: Unterzeichner des Vertragspartners2
+    unterzeichnervp2: Optional[List[Unterschrift]] = []
+    #: Beschreibung zum Vertrag
+    beschreibung: Optional[str] = None
diff --git a/src/bo4e/bo/vertrag.py b/src/bo4e/bo/vertrag.py
index 7decad40d..cd946695d 100644
--- a/src/bo4e/bo/vertrag.py
+++ b/src/bo4e/bo/vertrag.py
@@ -38,6 +38,7 @@ class Vertrag(Geschaeftsobjekt):
 
     # required attributes
     bo_typ: BoTyp = BoTyp.VERTRAG
+    # pylint: disable=duplicate-code
     #: Eine im Verwendungskontext eindeutige Nummer für den Vertrag
     vertragsnummer: str
     #: Hier ist festgelegt, um welche Art von Vertrag es sich handelt.


Test patch:
diff --git a/tests/test_buendelvertrag.py b/tests/test_buendelvertrag.py
index 202a3cd01..eec4908a3 100644
--- a/tests/test_buendelvertrag.py
+++ b/tests/test_buendelvertrag.py
@@ -1,23 +1,204 @@
+from datetime import datetime, timezone
+from typing import Any, Dict
+
 import pytest
 from pydantic import ValidationError
 
 from bo4e.bo.buendelvertrag import Buendelvertrag
+from bo4e.bo.geschaeftspartner import Geschaeftspartner
+from bo4e.com.adresse import Adresse
+from bo4e.com.unterschrift import Unterschrift
+from bo4e.com.vertragskonditionen import Vertragskonditionen
+from bo4e.enum.anrede import Anrede
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.geschaeftspartnerrolle import Geschaeftspartnerrolle
+from bo4e.enum.kontaktart import Kontaktart
+from bo4e.enum.landescode import Landescode
+from bo4e.enum.sparte import Sparte
+from bo4e.enum.vertragsart import Vertragsart
+from bo4e.enum.vertragsstatus import Vertragsstatus
 from tests.serialization_helper import assert_serialization_roundtrip
 from tests.test_vertrag import TestVertrag
 
 
 class TestBuendelvertrag:
+    _vertragspartner1 = Geschaeftspartner(
+        anrede=Anrede.FRAU,
+        name1="van der Waal",
+        name2="Helga",
+        name3=None,
+        gewerbekennzeichnung=True,
+        kontaktweg=[Kontaktart.SMS],
+        umsatzsteuer_id="DE267311963",
+        glaeubiger_id="DE98ZZZ09999999999",
+        e_mail_adresse="helga.waal@bo4e.de",
+        website="bo4e.de",
+        geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
+        partneradresse=Adresse(
+            postleitzahl="33333",
+            ort="Quadrat-Ichendorf",
+            strasse="Kubikstraße",
+            hausnummer="4",
+        ),
+    )
+    _vertragspartner2 = Geschaeftspartner(
+        name1="Eckart",
+        name2="Björn",
+        gewerbekennzeichnung=False,
+        geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
+        partneradresse=Adresse(
+            postleitzahl="24211",
+            ort="Preetz",
+            strasse="Am Markt",
+            hausnummer="67",
+        ),
+    )
+    _vertragspartner1_dict: Dict[str, Any] = {
+        "anrede": Anrede.FRAU,
+        "name1": "van der Waal",
+        "name2": "Helga",
+        "name3": None,
+        "gewerbekennzeichnung": True,
+        "kontaktweg": [Kontaktart.SMS],
+        "umsatzsteuerId": "DE267311963",
+        "glaeubigerId": "DE98ZZZ09999999999",
+        "eMailAdresse": "helga.waal@bo4e.de",
+        "website": "bo4e.de",
+        "geschaeftspartnerrolle": [Geschaeftspartnerrolle.DIENSTLEISTER],
+        "partneradresse": {
+            "postleitzahl": "33333",
+            "ort": "Quadrat-Ichendorf",
+            "strasse": "Kubikstraße",
+            "hausnummer": "4",
+            "postfach": None,
+            "adresszusatz": None,
+            "coErgaenzung": None,
+            "landescode": Landescode.DE,  # type:ignore[attr-defined]
+        },
+        "versionstruktur": "2",
+        "boTyp": BoTyp.GESCHAEFTSPARTNER,
+        "externeReferenzen": [],
+        "hrnummer": None,
+        "amtsgericht": None,
+    }
+    _vertragspartner2_dict: Dict[str, Any] = {
+        "name1": "Eckart",
+        "name2": "Björn",
+        "gewerbekennzeichnung": False,
+        "geschaeftspartnerrolle": [Geschaeftspartnerrolle.DIENSTLEISTER],
+        "partneradresse": {
+            "postleitzahl": "24211",
+            "ort": "Preetz",
+            "strasse": "Am Markt",
+            "hausnummer": "67",
+            "postfach": None,
+            "adresszusatz": None,
+            "coErgaenzung": None,
+            "landescode": Landescode.DE,  # type:ignore[attr-defined]
+        },
+        "versionstruktur": "2",
+        "boTyp": BoTyp.GESCHAEFTSPARTNER,
+        "externeReferenzen": [],
+        "anrede": None,
+        "name3": None,
+        "hrnummer": None,
+        "amtsgericht": None,
+        "kontaktweg": [],
+        "umsatzsteuerId": None,
+        "glaeubigerId": None,
+        "eMailAdresse": None,
+        "website": None,
+    }
+
     @pytest.mark.parametrize(
-        "buendelvertrag",
+        "buendelvertrag, expected_dict",
         [
-            pytest.param(Buendelvertrag(einzelvertraege=[TestVertrag().get_example_vertrag()])),
+            pytest.param(
+                Buendelvertrag(
+                    vertragsnummer="1234567890",
+                    vertragsart=Vertragsart.NETZNUTZUNGSVERTRAG,
+                    vertragsstatus=Vertragsstatus.AKTIV,
+                    sparte=Sparte.STROM,
+                    vertragsbeginn=datetime(2021, 4, 30, 13, 45, tzinfo=timezone.utc),
+                    vertragsende=datetime(2200, 4, 30, 13, 45, tzinfo=timezone.utc),
+                    vertragspartner1=_vertragspartner1,
+                    vertragspartner2=_vertragspartner2,
+                ),
+                {
+                    "vertragsnummer": "1234567890",
+                    "vertragsart": Vertragsart.NETZNUTZUNGSVERTRAG,
+                    "vertragsstatus": Vertragsstatus.AKTIV,
+                    "sparte": Sparte.STROM,
+                    "vertragsbeginn": datetime(2021, 4, 30, 13, 45, tzinfo=timezone.utc),
+                    "vertragsende": datetime(2200, 4, 30, 13, 45, tzinfo=timezone.utc),
+                    "vertragspartner1": _vertragspartner1_dict,
+                    "vertragspartner2": _vertragspartner2_dict,
+                    "versionstruktur": "2",
+                    "boTyp": BoTyp.BUENDELVERTRAG,
+                    "externeReferenzen": [],
+                    "einzelvertraege": [],
+                    "vertragskonditionen": [],
+                    "unterzeichnervp1": [],
+                    "unterzeichnervp2": [],
+                    "beschreibung": None,
+                },
+                id="minimal fields",
+            ),
+            pytest.param(
+                Buendelvertrag(
+                    vertragsnummer="1234567890",
+                    vertragsart=Vertragsart.NETZNUTZUNGSVERTRAG,
+                    vertragsstatus=Vertragsstatus.AKTIV,
+                    sparte=Sparte.STROM,
+                    vertragsbeginn=datetime(2021, 4, 30, 13, 45, tzinfo=timezone.utc),
+                    vertragsende=datetime(2200, 4, 30, 13, 45, tzinfo=timezone.utc),
+                    vertragspartner1=_vertragspartner1,
+                    vertragspartner2=_vertragspartner2,
+                    einzelvertraege=[TestVertrag().get_example_vertrag()],
+                    vertragskonditionen=[Vertragskonditionen(beschreibung="Hello World")],
+                    unterzeichnervp1=[Unterschrift(name="Helga van der Waal")],
+                    unterzeichnervp2=[Unterschrift(name="Björn oder so"), Unterschrift(name="Zweiter Typ")],
+                    beschreibung="Das ist ein Bündelvertrag mit allen optionalen Feldern ausgefüllt.",
+                ),
+                {
+                    "versionstruktur": "2",
+                    "boTyp": BoTyp.BUENDELVERTRAG,
+                    "externeReferenzen": [],
+                    "vertragsnummer": "1234567890",
+                    "vertragsart": Vertragsart.NETZNUTZUNGSVERTRAG,
+                    "vertragsstatus": Vertragsstatus.AKTIV,
+                    "sparte": Sparte.STROM,
+                    "vertragsbeginn": datetime(2021, 4, 30, 13, 45, tzinfo=timezone.utc),
+                    "vertragsende": datetime(2200, 4, 30, 13, 45, tzinfo=timezone.utc),
+                    "vertragspartner1": _vertragspartner1_dict,
+                    "vertragspartner2": _vertragspartner2_dict,
+                    "einzelvertraege": [TestVertrag().get_example_vertrag_expected_dict()],
+                    "vertragskonditionen": [
+                        {
+                            "beschreibung": "Hello World",
+                            "anzahlAbschlaege": None,
+                            "vertragslaufzeit": None,
+                            "kuendigungsfrist": None,
+                            "vertragsverlaengerung": None,
+                            "abschlagszyklus": None,
+                        }
+                    ],
+                    "unterzeichnervp1": [{"name": "Helga van der Waal", "ort": None, "datum": None}],
+                    "unterzeichnervp2": [
+                        {"name": "Björn oder so", "ort": None, "datum": None},
+                        {"name": "Zweiter Typ", "ort": None, "datum": None},
+                    ],
+                    "beschreibung": "Das ist ein Bündelvertrag mit allen optionalen Feldern ausgefüllt.",
+                },
+                id="maximal fields",
+            ),
         ],
     )
-    def test_serialization_roundtrip(self, buendelvertrag: Buendelvertrag) -> None:
-        assert_serialization_roundtrip(buendelvertrag)
+    def test_serialization_roundtrip(self, buendelvertrag: Buendelvertrag, expected_dict: Dict[str, Any]) -> None:
+        assert_serialization_roundtrip(buendelvertrag, expected_dict)
 
     def test_missing_required_attribute(self) -> None:
         with pytest.raises(ValidationError) as excinfo:
             _ = Buendelvertrag()  # type: ignore[call-arg]
 
-        assert "1 validation error" in str(excinfo.value)
+        assert "8 validation errors" in str(excinfo.value)
diff --git a/tests/test_vertrag.py b/tests/test_vertrag.py
index 271d7d31a..70f81bd13 100644
--- a/tests/test_vertrag.py
+++ b/tests/test_vertrag.py
@@ -1,4 +1,5 @@
 from datetime import datetime, timezone
+from typing import Any, Dict, List
 
 import pytest
 from pydantic import ValidationError
@@ -13,6 +14,7 @@
 from bo4e.enum.botyp import BoTyp
 from bo4e.enum.geschaeftspartnerrolle import Geschaeftspartnerrolle
 from bo4e.enum.kontaktart import Kontaktart
+from bo4e.enum.landescode import Landescode
 from bo4e.enum.sparte import Sparte
 from bo4e.enum.vertragsart import Vertragsart
 from bo4e.enum.vertragsstatus import Vertragsstatus
@@ -62,6 +64,72 @@ class TestVertrag:
             vertragsteilende=datetime(2021, 6, 5, tzinfo=timezone.utc),
         )
     ]
+    _vertragspartner1_dict: Dict[str, Any] = {
+        "versionstruktur": "2",
+        "boTyp": BoTyp.GESCHAEFTSPARTNER,
+        "externeReferenzen": [],
+        "name1": "von Sinnen",
+        "gewerbekennzeichnung": True,
+        "geschaeftspartnerrolle": [Geschaeftspartnerrolle.DIENSTLEISTER],
+        "anrede": "FRAU",
+        "name2": "Helga",
+        "name3": None,
+        "hrnummer": None,
+        "amtsgericht": None,
+        "kontaktweg": [Kontaktart.E_MAIL],
+        "umsatzsteuerId": "DE267311963",
+        "glaeubigerId": "DE98ZZZ09999999999",
+        "eMailAdresse": "test@bo4e.de",
+        "website": "bo4e.de",
+        "partneradresse": {
+            "postleitzahl": "24306",
+            "ort": "Plön",
+            "strasse": "Kirchstraße",
+            "hausnummer": "3",
+            "postfach": None,
+            "adresszusatz": None,
+            "coErgaenzung": None,
+            "landescode": Landescode.DE,  # type:ignore[attr-defined]
+        },
+    }
+    _vertragspartner2_dict: Dict[str, Any] = {
+        "versionstruktur": "2",
+        "boTyp": BoTyp.GESCHAEFTSPARTNER,
+        "externeReferenzen": [],
+        "name1": "Eckart",
+        "gewerbekennzeichnung": False,
+        "geschaeftspartnerrolle": [Geschaeftspartnerrolle.DIENSTLEISTER],
+        "anrede": None,
+        "name2": "Björn",
+        "name3": None,
+        "hrnummer": None,
+        "amtsgericht": None,
+        "kontaktweg": [],
+        "umsatzsteuerId": None,
+        "glaeubigerId": None,
+        "eMailAdresse": None,
+        "website": None,
+        "partneradresse": {
+            "postleitzahl": "24211",
+            "ort": "Preetz",
+            "strasse": "Am Markt",
+            "hausnummer": "67",
+            "postfach": None,
+            "adresszusatz": None,
+            "coErgaenzung": None,
+            "landescode": Landescode.DE,  # type:ignore[attr-defined]
+        },
+    }
+    _vertragsteile_dict: List[Dict[str, Any]] = [
+        {
+            "vertragsteilbeginn": datetime(2021, 4, 30, tzinfo=timezone.utc),
+            "vertragsteilende": datetime(2021, 6, 5, tzinfo=timezone.utc),
+            "lokation": None,
+            "vertraglichFixierteMenge": None,
+            "minimaleAbnahmemenge": None,
+            "maximaleAbnahmemenge": None,
+        }
+    ]
 
     def get_example_vertrag(self) -> Vertrag:
         return Vertrag(
@@ -76,6 +144,26 @@ def get_example_vertrag(self) -> Vertrag:
             vertragsteile=self._vertragsteile,
         )
 
+    def get_example_vertrag_expected_dict(self) -> Dict[str, Any]:
+        return {
+            "vertragsnummer": self._vertragsnummer,
+            "vertragsart": self._vertragsart,
+            "vertragsstatus": self._vertragsstatus,
+            "sparte": self._sparte,
+            "vertragsbeginn": self._vertragsbeginn,
+            "vertragsende": self._vertragsende,
+            "vertragspartner1": self._vertragspartner1_dict,
+            "vertragspartner2": self._vertragspartner2_dict,
+            "vertragsteile": self._vertragsteile_dict,
+            "versionstruktur": "2",
+            "boTyp": BoTyp.VERTRAG,
+            "externeReferenzen": [],
+            "beschreibung": None,
+            "vertragskonditionen": None,
+            "unterzeichnervp1": None,
+            "unterzeichnervp2": None,
+        }
+
     def test_serialisation_only_required_attributes(self) -> None:
         """
         Test de-/serialisation of Vertrag with minimal attributes.