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

Issue:
Implement BO Fremdkosten
https://www.bo4e.de/dokumentation/geschaeftsobjekte/bo-fremdkosten
requires
- #144 
- #180 


Discussion / hints:

Fix patch:
diff --git a/src/bo4e/bo/fremdkosten.py b/src/bo4e/bo/fremdkosten.py
new file mode 100644
index 000000000..5668b3e8d
--- /dev/null
+++ b/src/bo4e/bo/fremdkosten.py
@@ -0,0 +1,57 @@
+"""
+Contains Fremdkosten class and corresponding marshmallow schema for de-/serialization
+"""
+from typing import List, Optional
+
+import attr
+from marshmallow import fields
+
+from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
+from bo4e.com.betrag import Betrag, BetragSchema
+from bo4e.com.fremdkostenblock import Fremdkostenblock, FremdkostenblockSchema
+from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
+from bo4e.enum.botyp import BoTyp
+
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Fremdkosten(Geschaeftsobjekt):
+    """
+    Mit diesem BO werden die Fremdkosten, beispielsweise für eine Angebotserstellung oder eine Rechnungsprüfung,
+    übertragen.
+    Die Fremdkosten enthalten dabei alle Kostenblöcke, die von anderen Marktteilnehmern oder Instanzen erhoben werden.
+    """
+
+    # required attributes
+    bo_typ: BoTyp = attr.ib(default=BoTyp.FREMDKOSTEN)
+    #: Für diesen Zeitraum wurden die Kosten ermittelt
+    gueltigkeit: Zeitraum = attr.ib(validator=attr.validators.instance_of(Zeitraum))
+    # optional attributes
+    #: Die Gesamtsumme über alle Kostenblöcke und -positionen
+    summe_kosten: Optional[Betrag] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Betrag)), default=None
+    )
+    #: In Kostenblöcken werden Kostenpositionen zusammengefasst. Beispiele: Netzkosten, Umlagen, Steuern etc
+    kostenbloecke: Optional[List[Fremdkostenblock]] = attr.ib(
+        default=None,
+        validator=attr.validators.optional(
+            attr.validators.deep_iterable(
+                member_validator=attr.validators.instance_of(Fremdkostenblock),
+                iterable_validator=attr.validators.instance_of(list),
+            )
+        ),
+    )
+
+
+class FremdkostenSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Fremdkosten
+    """
+
+    class_name = Fremdkosten
+    # required attributes
+    gueltigkeit = fields.Nested(ZeitraumSchema)
+
+    # optional attributes
+    summe_kosten = fields.Nested(BetragSchema, load_default=None)
+    kostenbloecke = fields.List(fields.Nested(FremdkostenblockSchema), load_default=None)
diff --git a/src/bo4e/enum/botyp.py b/src/bo4e/enum/botyp.py
index dcc5e463f..84def944e 100644
--- a/src/bo4e/enum/botyp.py
+++ b/src/bo4e/enum/botyp.py
@@ -12,6 +12,7 @@ class BoTyp(StrEnum):
     AUSSCHREIBUNG = "AUSSCHREIUNG"
     BUENDELVERTRAG = "BUENDELVERTRAG"
     ENERGIEMENGE = "ENERGIEMENGE"
+    FREMDKOSTEN = "FREMDKOSTEN"
     # It is practical to use the BoTyp Enum as discriminator in the database.
     # Therefore, we added one additional entry for GESCHAEFTSOBJEKT
     # This is not defined by the documentation!


Test patch:
diff --git a/tests/test_fremdkosten.py b/tests/test_fremdkosten.py
new file mode 100644
index 000000000..e0231a80d
--- /dev/null
+++ b/tests/test_fremdkosten.py
@@ -0,0 +1,32 @@
+import pytest  # type:ignore[import]
+
+from bo4e.bo.fremdkosten import Fremdkosten, FremdkostenSchema
+from bo4e.com.fremdkostenblock import Fremdkostenblock
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+from tests.test_betrag import example_betrag  # type:ignore[import]
+from tests.test_zeitraum import example_zeitraum  # type:ignore[import]
+
+
+class TestFremdkosten:
+    @pytest.mark.parametrize(
+        "fremdkosten",
+        [
+            pytest.param(
+                Fremdkosten(
+                    gueltigkeit=example_zeitraum,
+                    summe_kosten=example_betrag,
+                    kostenbloecke=[Fremdkostenblock(kostenblockbezeichnung="teststring")],
+                )
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, fremdkosten: Fremdkosten):
+        """
+        Test de-/serialisation
+        """
+        assert_serialization_roundtrip(fremdkosten, FremdkostenSchema())
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Fremdkosten()
+        assert "missing 1 required" in str(excinfo.value)