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

Issue:
Implement COM Kostenposition (and let Fremdkostenposition inherit from it)
Fixes #170
Fixes #285

⚠️ before merging into master please update the branch related #291 with the updates from this branch
Implement COM Kostenblock
https://www.bo4e.de/dokumentation/komponenten/com-kostenblock
- #144 
- #170


Discussion / hints:

Fix patch:
diff --git a/src/bo4e/com/kostenblock.py b/src/bo4e/com/kostenblock.py
new file mode 100644
index 000000000..bb4a28695
--- /dev/null
+++ b/src/bo4e/com/kostenblock.py
@@ -0,0 +1,56 @@
+"""
+Contains Kostenblock 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.kostenposition import Kostenposition, KostenpositionSchema
+
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Kostenblock(COM):
+    """
+    Mit dieser Komponente werden mehrere Kostenpositionen zusammengefasst.
+    """
+
+    # 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
+    #: Die Summe aller Kostenpositionen dieses Blocks
+    summe_kostenblock: Optional[Betrag] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Betrag))
+    )
+
+    kostenpositionen: Optional[List[Kostenposition]] = attr.ib(
+        default=None,
+        validator=attr.validators.optional(
+            attr.validators.deep_iterable(
+                member_validator=attr.validators.instance_of(Kostenposition),
+                iterable_validator=attr.validators.instance_of(list),
+            )
+        ),
+    )
+    """
+    Hier sind die Details zu einer Kostenposition aufgeführt. Z.B.:
+    Alliander Netz Heinsberg GmbH, 01.02.2018, 31.12.2018, Arbeitspreis HT, 3.660 kWh, 5,8200 ct/kWh, 213,01 €
+    """
+
+
+class KostenblockSchema(COMSchema):
+    """
+    Schema for de-/serialization of Kostenblock
+    """
+
+    class_name = Kostenblock
+    # required attributes
+    kostenblockbezeichnung = fields.Str()
+    # optional attributes
+    summe_kostenblock = fields.Nested(BetragSchema, allow_none=True)
+    kostenpositionen = fields.List(fields.Nested(KostenpositionSchema), allow_none=True)


Test patch:
diff --git a/tests/test_kostenblock.py b/tests/test_kostenblock.py
new file mode 100644
index 000000000..bc6175fd1
--- /dev/null
+++ b/tests/test_kostenblock.py
@@ -0,0 +1,92 @@
+from decimal import Decimal
+
+import pytest  # type:ignore[import]
+
+from bo4e.com.betrag import Betrag
+from bo4e.com.kostenblock import Kostenblock, KostenblockSchema
+from bo4e.com.kostenposition import Kostenposition
+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]
+from tests.test_sigmoidparameter import example_sigmoidparameter  # type:ignore[import]
+
+
+class TestKostenblock:
+    @pytest.mark.parametrize(
+        "kostenblock, expected_json_dict",
+        [
+            pytest.param(
+                Kostenblock(
+                    kostenblockbezeichnung="Mein Kostenblock",
+                ),
+                {
+                    "kostenblockbezeichnung": "Mein Kostenblock",
+                    "summeKostenblock": None,
+                    "kostenpositionen": None,
+                },
+                id="minimal",
+            ),
+            pytest.param(
+                Kostenblock(
+                    kostenblockbezeichnung="Mein Kostenblock",
+                    summe_kostenblock=Betrag(
+                        waehrung=Waehrungscode.EUR,
+                        wert=Decimal(12.5),
+                    ),
+                    kostenpositionen=[
+                        Kostenposition(
+                            positionstitel="Mudders kostenposition",
+                            artikelbezeichnung="Dei Mudder ihr kostenposition",
+                            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),
+                            ),
+                        ),
+                    ],
+                ),
+                {
+                    "kostenblockbezeichnung": "Mein Kostenblock",
+                    "kostenpositionen": [
+                        {
+                            "positionstitel": "Mudders kostenposition",
+                            "einzelpreis": {
+                                "einheit": "EUR",
+                                "status": "ENDGUELTIG",
+                                "bezugswert": "KWH",
+                                "wert": "3.5",
+                            },
+                            "menge": None,
+                            "zeitmenge": None,
+                            "von": None,
+                            "artikelbezeichnung": "Dei Mudder ihr kostenposition",
+                            "artikeldetail": None,
+                            "bis": None,
+                            "betragKostenposition": {"waehrung": "EUR", "wert": "12.5"},
+                        }
+                    ],
+                    "summeKostenblock": {"waehrung": "EUR", "wert": "12.5"},
+                },
+                id="maximal",
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, kostenblock: Kostenblock, expected_json_dict: dict):
+        """
+        Test de-/serialisation of kostenblock.
+        """
+        assert_serialization_roundtrip(kostenblock, KostenblockSchema(), expected_json_dict)
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Kostenblock()
+
+        assert "missing 1 required" in str(excinfo.value)
diff --git a/tests/test_kostenposition.py b/tests/test_kostenposition.py
index bddefbd61..433d8fb6e 100644
--- a/tests/test_kostenposition.py
+++ b/tests/test_kostenposition.py
@@ -84,7 +84,7 @@ class TestKostenposition:
     )
     def test_serialization_roundtrip(self, kostenposition: Kostenposition, expected_json_dict: dict):
         """
-        Test de-/serialisation of Kostenposition.
+        Test de-/serialisation of Kostenposition
         """
         assert_serialization_roundtrip(kostenposition, KostenpositionSchema(), expected_json_dict)