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

Issue:
Implement COM Fremdkostenblock
https://www.bo4e.de/dokumentation/komponenten/com-fremdkostenblock
- #144 
- #146 


Discussion / hints:

Fix patch:
diff --git a/src/bo4e/com/fremdkostenblock.py b/src/bo4e/com/fremdkostenblock.py
new file mode 100644
index 000000000..2f81e5a09
--- /dev/null
+++ b/src/bo4e/com/fremdkostenblock.py
@@ -0,0 +1,61 @@
+"""
+Contains Fremdkostenblock class
+and corresponding marshmallow schema for de-/serialization
+"""
+
+from typing import List, Optional
+
+import attr
+from marshmallow import fields
+
+from bo4e.com.betrag import Betrag, BetragSchema
+from bo4e.com.com import COM, COMSchema
+from bo4e.com.fremdkostenposition import Fremdkostenposition, FremdkostenpositionSchema
+
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Fremdkostenblock(COM):
+    """
+    Komponente zur Abbildung eines Kostenblocks in den Fremdkosten
+    """
+
+    # required attributes
+    #: Bezeichnung für einen Kostenblock. Z.B. Netzkosten, Messkosten, Umlagen, etc.
+    kostenblockbezeichnung: str = attr.ib(validator=attr.validators.instance_of(str))
+
+    # optional attributes
+    kostenpositionen: Optional[List[Fremdkostenposition]] = attr.ib(
+        default=None,
+        validator=attr.validators.optional(
+            attr.validators.deep_iterable(
+                member_validator=attr.validators.instance_of(Fremdkostenposition),
+                iterable_validator=attr.validators.instance_of(list),
+            )
+        ),
+    )
+    """
+    Hier sind die Details zu einer Kostenposition aufgeführt. Z.B.:
+    Alliander Netz Heinsberg GmbH, 2018-02-01, 2019-01-01, Arbeitspreis HT, 3.660 kWh,
+    5,8200 ct/kWh, 213,01 €
+    """
+
+    #: Die Summe aller Kostenpositionen dieses Blocks
+    summe_kostenblock: Optional[Betrag] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Betrag))
+    )
+    # todo: write validator fo this sum, see https://github.com/Hochfrequenz/BO4E-python/issues/324
+
+
+class FremdkostenblockSchema(COMSchema):
+    """
+    Schema for de-/serialization of Fremdkostenblock
+    """
+
+    class_name = Fremdkostenblock
+    # required attributes
+    kostenblockbezeichnung = fields.Str()
+
+    # optional attributes
+    kostenpositionen = fields.List(fields.Nested(FremdkostenpositionSchema), load_default=None)
+    summe_kostenblock = fields.Nested(BetragSchema, load_default=None)


Test patch:
diff --git a/tests/test_fremdkostenblock.py b/tests/test_fremdkostenblock.py
new file mode 100644
index 000000000..9eb8f81ec
--- /dev/null
+++ b/tests/test_fremdkostenblock.py
@@ -0,0 +1,92 @@
+from decimal import Decimal
+
+import pytest  # type:ignore[import]
+
+from bo4e.com.betrag import Betrag
+from bo4e.com.fremdkostenblock import Fremdkostenblock, FremdkostenblockSchema
+from bo4e.com.fremdkostenposition import Fremdkostenposition
+from bo4e.com.preis import Preis
+from bo4e.enum.mengeneinheit import Mengeneinheit
+from bo4e.enum.preisstatus import Preisstatus
+from bo4e.enum.waehrungscode import Waehrungscode
+from bo4e.enum.waehrungseinheit import Waehrungseinheit
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+
+
+class TestFremdkostenblock:
+    @pytest.mark.parametrize(
+        "fremdkostenblock, expected_json_dict",
+        [
+            pytest.param(
+                Fremdkostenblock(
+                    kostenblockbezeichnung="teststring",
+                    kostenpositionen=[
+                        Fremdkostenposition(
+                            positionstitel="fremdkostenblocktitel",
+                            artikelbezeichnung="bsp",
+                            einzelpreis=Preis(
+                                wert=Decimal(3.50),
+                                einheit=Waehrungseinheit.EUR,
+                                bezugswert=Mengeneinheit.KWH,
+                                status=Preisstatus.ENDGUELTIG,
+                            ),
+                            betrag_kostenposition=Betrag(
+                                waehrung=Waehrungscode.EUR,
+                                wert=Decimal(12.5),
+                            ),
+                        )
+                    ],
+                    summe_kostenblock=Betrag(
+                        waehrung=Waehrungscode.EUR,
+                        wert=Decimal(98240),
+                    ),
+                ),
+                {
+                    "kostenblockbezeichnung": "teststring",
+                    "kostenpositionen": [
+                        {
+                            "marktpartnercode": None,
+                            "positionstitel": "fremdkostenblocktitel",
+                            "einzelpreis": {
+                                "wert": "3.5",
+                                "einheit": "EUR",
+                                "bezugswert": "KWH",
+                                "status": "ENDGUELTIG",
+                            },
+                            "bis": None,
+                            "menge": None,
+                            "zeitmenge": None,
+                            "artikelbezeichnung": "bsp",
+                            "marktpartnername": None,
+                            "artikeldetail": None,
+                            "von": None,
+                            "linkPreisblatt": None,
+                            "betragKostenposition": {"wert": "12.5", "waehrung": "EUR"},
+                            "gebietcodeEic": None,
+                        }
+                    ],
+                    "summeKostenblock": {"wert": "98240", "waehrung": "EUR"},
+                },
+                id="maximal attributes",
+            ),
+            pytest.param(
+                Fremdkostenblock(kostenblockbezeichnung="teststring"),
+                {
+                    "kostenblockbezeichnung": "teststring",
+                    "kostenpositionen": None,
+                    "summeKostenblock": None,
+                },
+                id="minimal attributes",
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, fremdkostenblock, expected_json_dict):
+        """
+        Test de-/serialisation of Fremdkostenblock
+        """
+        assert_serialization_roundtrip(fremdkostenblock, FremdkostenblockSchema(), expected_json_dict)
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Fremdkostenblock()
+        assert "missing 1 required" in str(excinfo.value)