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

Issue:
Implement BO Ausschreibung
https://www.bo4e.de/dokumentation/geschaeftsobjekte/bo-ausschreibung
requires
- #156 
- #129 
- #94 
- #108 
- 



Discussion / hints:

Fix patch:
diff --git a/src/bo4e/bo/ausschreibung.py b/src/bo4e/bo/ausschreibung.py
new file mode 100644
index 000000000..a61b228e3
--- /dev/null
+++ b/src/bo4e/bo/ausschreibung.py
@@ -0,0 +1,95 @@
+"""
+Contains Ausschreibung class and corresponding marshmallow schema for de-/serialization
+"""
+from datetime import datetime
+from typing import List, Optional
+
+import attr
+from marshmallow import fields
+from marshmallow_enum import EnumField  # type:ignore[import]
+
+from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
+from bo4e.bo.geschaeftspartner import Geschaeftspartner, GeschaeftspartnerSchema
+from bo4e.com.ausschreibungslos import Ausschreibungslos, AusschreibungslosSchema
+from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
+from bo4e.enum.ausschreibungsportal import Ausschreibungsportal
+from bo4e.enum.ausschreibungsstatus import Ausschreibungsstatus
+from bo4e.enum.ausschreibungstyp import Ausschreibungstyp
+from bo4e.enum.botyp import BoTyp
+from bo4e.validators import check_list_length_at_least_one
+
+
+# pylint: disable=too-few-public-methods, too-many-instance-attributes
+@attr.s(auto_attribs=True, kw_only=True)
+class Ausschreibung(Geschaeftsobjekt):
+    """
+    Das BO Ausschreibung dient zur detaillierten Darstellung von ausgeschriebenen Energiemengen in der Energiewirtschaft
+    """
+
+    # required attributes
+    bo_typ: BoTyp = attr.ib(default=BoTyp.AUSSCHREIBUNG)
+    #: Vom Herausgeber der Ausschreibung vergebene eindeutige Nummer
+    ausschreibungsnummer: str = attr.ib(validator=attr.validators.instance_of(str))
+    #: Aufzählung für die Typisierung von Ausschreibungen
+    ausschreibungstyp: Ausschreibungstyp = attr.ib(validator=attr.validators.instance_of(Ausschreibungstyp))
+    #: Bezeichnungen für die Ausschreibungsphasen
+    ausschreibungsstatus: Ausschreibungsstatus = attr.ib(validator=attr.validators.instance_of(Ausschreibungsstatus))
+    #: Kennzeichen, ob die Ausschreibung kostenpflichtig ist
+    kostenpflichtig: bool = attr.ib(validator=attr.validators.instance_of(bool))
+    #: Gibt den Veröffentlichungszeitpunkt der Ausschreibung an
+    veroeffentlichungszeitpunkt: datetime = attr.ib(validator=attr.validators.instance_of(datetime))
+    ausschreibender: Geschaeftspartner = attr.ib(validator=attr.validators.instance_of(Geschaeftspartner))
+    """
+    Mit diesem Objekt können Geschäftspartner übertragen werden.
+    Sowohl Unternehmen, als auch Privatpersonen können Geschäftspartner sein
+    """
+    abgabefrist: Zeitraum = attr.ib(validator=attr.validators.instance_of(Zeitraum))
+    """
+    Diese Komponente wird zur Abbildung von Zeiträumen in Form von Dauern oder der Angabe von Start und Ende verwendet.
+    Es muss daher entweder eine Dauer oder ein Zeitraum in Form von Start und Ende angegeben sein
+    """
+    bindefrist: Zeitraum = attr.ib(validator=attr.validators.instance_of(Zeitraum))
+    """
+    Diese Komponente wird zur Abbildung von Zeiträumen in Form von Dauern oder der Angabe von Start und Ende verwendet.
+    Es muss daher entweder eine Dauer oder ein Zeitraum in Form von Start und Ende angegeben sein
+    """
+    #: Die einzelnen Lose, aus denen sich die Ausschreibung zusammensetzt
+    lose: List[Ausschreibungslos] = attr.ib(
+        validator=attr.validators.deep_iterable(
+            member_validator=attr.validators.instance_of(Ausschreibungslos),
+            iterable_validator=check_list_length_at_least_one,
+        )
+    )
+
+    # optional attributes
+    #: Aufzählung der unterstützten Ausschreibungsportale
+    ausschreibungportal: Optional[Ausschreibungsportal] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Ausschreibungsportal))
+    )
+    #: Internetseite, auf der die Ausschreibung veröffentlicht wurde (falls vorhanden)
+    webseite: Optional[str] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(str))
+    )
+
+
+class AusschreibungSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Ausschreibung
+    """
+
+    class_name = Ausschreibung
+
+    # required attributes
+    ausschreibungsnummer = fields.Str()
+    ausschreibungstyp = EnumField(Ausschreibungstyp)
+    ausschreibungsstatus = EnumField(Ausschreibungsstatus)
+    kostenpflichtig = fields.Bool()
+    veroeffentlichungszeitpunkt = fields.DateTime()
+    ausschreibender = fields.Nested(GeschaeftspartnerSchema)
+    abgabefrist = fields.Nested(ZeitraumSchema)
+    bindefrist = fields.Nested(ZeitraumSchema)
+    lose = fields.List(fields.Nested(AusschreibungslosSchema))
+
+    # optional attributes
+    ausschreibungportal = EnumField(Ausschreibungsportal, allow_none=True)
+    webseite = fields.Str(allow_none=True)


Test patch:
diff --git a/tests/test_ausschreibung.py b/tests/test_ausschreibung.py
new file mode 100644
index 000000000..4d9f4da9e
--- /dev/null
+++ b/tests/test_ausschreibung.py
@@ -0,0 +1,70 @@
+from datetime import datetime, timezone
+
+import pytest  # type:ignore[import]
+
+from bo4e.bo.ausschreibung import Ausschreibung, AusschreibungSchema
+from bo4e.bo.geschaeftspartner import Geschaeftspartner
+from bo4e.enum.ausschreibungsportal import Ausschreibungsportal
+from bo4e.enum.ausschreibungsstatus import Ausschreibungsstatus
+from bo4e.enum.ausschreibungstyp import Ausschreibungstyp
+from bo4e.enum.geschaeftspartnerrolle import Geschaeftspartnerrolle
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+from tests.test_adresse import example_adresse  # type:ignore[import]
+from tests.test_ausschreibungslos import example_ausschreibungslos  # type:ignore[import]
+from tests.test_zeitraum import example_zeitraum  # type:ignore[import]
+
+
+class TestAusschreibung:
+    @pytest.mark.parametrize(
+        "ausschreibung",
+        [
+            pytest.param(
+                Ausschreibung(
+                    ausschreibungsnummer="239230",
+                    ausschreibungstyp=Ausschreibungstyp.PRIVATRECHTLICH,
+                    ausschreibungsstatus=Ausschreibungsstatus.PHASE3,
+                    kostenpflichtig=True,
+                    ausschreibungportal=Ausschreibungsportal.BMWI,
+                    webseite="https://meineausschreibungswebsite.inv/",
+                    veroeffentlichungszeitpunkt=datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
+                    abgabefrist=example_zeitraum,
+                    bindefrist=example_zeitraum,
+                    ausschreibender=Geschaeftspartner(
+                        name1="Batman",
+                        gewerbekennzeichnung=True,
+                        geschaeftspartnerrolle=[Geschaeftspartnerrolle.LIEFERANT],
+                        partneradresse=example_adresse,
+                    ),
+                    lose=[example_ausschreibungslos],
+                ),
+                id="maximal attributes",
+            ),
+            pytest.param(
+                Ausschreibung(
+                    ausschreibungsnummer="239230",
+                    ausschreibungstyp=Ausschreibungstyp.PRIVATRECHTLICH,
+                    ausschreibungsstatus=Ausschreibungsstatus.PHASE3,
+                    kostenpflichtig=True,
+                    veroeffentlichungszeitpunkt=datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
+                    abgabefrist=example_zeitraum,
+                    bindefrist=example_zeitraum,
+                    ausschreibender=Geschaeftspartner(
+                        name1="Batman",
+                        gewerbekennzeichnung=True,
+                        geschaeftspartnerrolle=[Geschaeftspartnerrolle.LIEFERANT],
+                        partneradresse=example_adresse,
+                    ),
+                    lose=[example_ausschreibungslos],
+                ),
+                id="minimal attributes",
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, ausschreibung: Ausschreibung):
+        assert_serialization_roundtrip(ausschreibung, AusschreibungSchema())
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Ausschreibung()
+
+        assert "missing 9 required" in str(excinfo.value)
diff --git a/tests/test_ausschreibungslos.py b/tests/test_ausschreibungslos.py
index c16001efc..a7be27c27 100644
--- a/tests/test_ausschreibungslos.py
+++ b/tests/test_ausschreibungslos.py
@@ -13,31 +13,33 @@
 from tests.test_menge import example_menge, example_menge_dict  # type:ignore[import]
 from tests.test_zeitraum import example_zeitraum, example_zeitraum_dict  # type:ignore[import]
 
+example_ausschreibungslos = Ausschreibungslos(
+    losnummer="foo",
+    bezeichnung="bar",
+    bemerkung="asd",
+    preismodell=Preismodell.FESTPREIS,
+    energieart=Sparte.STROM,
+    wunsch_rechnungslegung=Rechnungslegung.MONATSRECHN,
+    wunsch_vertragsform=Vertragsform.DIREKT,
+    betreut_durch="Max Mustermann",
+    anzahl_lieferstellen=17,
+    lieferstellen=[example_ausschreibungsdetail],
+    gesamt_menge=example_menge,
+    wunsch_mindestmenge=example_menge,
+    wunsch_maximalmenge=example_menge,
+    lieferzeitraum=example_zeitraum,
+    wunsch_kuendingungsfrist=example_zeitraum,
+    wunsch_zahlungsziel=example_zeitraum,
+    wiederholungsintervall=example_zeitraum,
+)
+
 
 class TestAusschreibungslos:
     @pytest.mark.parametrize(
         "ausschreibungslos, expected_json_dict",
         [
             pytest.param(
-                Ausschreibungslos(
-                    losnummer="foo",
-                    bezeichnung="bar",
-                    bemerkung="asd",
-                    preismodell=Preismodell.FESTPREIS,
-                    energieart=Sparte.STROM,
-                    wunsch_rechnungslegung=Rechnungslegung.MONATSRECHN,
-                    wunsch_vertragsform=Vertragsform.DIREKT,
-                    betreut_durch="Max Mustermann",
-                    anzahl_lieferstellen=17,
-                    lieferstellen=[example_ausschreibungsdetail],
-                    gesamt_menge=example_menge,
-                    wunsch_mindestmenge=example_menge,
-                    wunsch_maximalmenge=example_menge,
-                    lieferzeitraum=example_zeitraum,
-                    wunsch_kuendingungsfrist=example_zeitraum,
-                    wunsch_zahlungsziel=example_zeitraum,
-                    wiederholungsintervall=example_zeitraum,
-                ),
+                example_ausschreibungslos,
                 {
                     "lieferzeitraum": example_zeitraum_dict,
                     "preismodell": "FESTPREIS",