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

Issue:
Implement COM Angebotsposition
https://www.bo4e.de/dokumentation/komponenten/com-angebotsposition

requires
- #145 
- #144 
for the validation keep #126 in mind


Discussion / hints:

Fix patch:
diff --git a/src/bo4e/com/angebotsposition.py b/src/bo4e/com/angebotsposition.py
new file mode 100644
index 000000000..d0537fc2b
--- /dev/null
+++ b/src/bo4e/com/angebotsposition.py
@@ -0,0 +1,57 @@
+"""
+Contains Angebotsposition class
+and corresponding marshmallow schema for de-/serialization
+"""
+
+import attr
+from marshmallow import fields, post_load
+
+from bo4e.com.betrag import Betrag, BetragSchema
+from bo4e.com.com import COM, COMSchema
+from bo4e.com.menge import Menge, MengeSchema
+from bo4e.com.preis import Preis, PreisSchema
+
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Angebotsposition(COM):
+    """
+    Unterhalb von Angebotsteilen sind die Angebotspositionen eingebunden.
+    Hier werden die angebotenen Bestandteile einzeln aufgeführt. Beispiel:
+    Positionsmenge: 4000 kWh
+    Positionspreis: 24,56 ct/kWh
+    Positionskosten: 982,40 EUR
+    """
+
+    # required attributes
+    #: Bezeichnung der jeweiligen Position des Angebotsteils
+    positionsbezeichnung: str = attr.ib(validator=attr.validators.instance_of(str))
+    #: Preis pro Einheit/Stückpreis des angebotenen Artikels.
+    positionspreis: Preis = attr.ib(validator=attr.validators.instance_of(Preis))
+
+    # optional attributes
+    #: Menge des angebotenen Artikels (z.B. Wirkarbeit in kWh), in dieser Angebotsposition
+    positionsmenge: Menge = attr.ib(default=None, validator=attr.validators.instance_of(Menge))
+    #: Kosten (positionspreis * positionsmenge) für diese Angebotsposition
+    positionskosten: Betrag = attr.ib(default=None, validator=attr.validators.instance_of(Betrag))
+
+    # for a preis = menge*times validation we first need to resolve
+    # https://github.com/Hochfrequenz/BO4E-python/issues/126
+
+
+class AngebotspositionSchema(COMSchema):
+    """
+    Schema for de-/serialization of Angebotsposition.
+    """
+
+    # required attributes
+    positionsbezeichnung = fields.String()
+    positionspreis = fields.Nested(PreisSchema)
+    positionsmenge = fields.Nested(MengeSchema, load_default=None)
+    positionskosten = fields.Nested(BetragSchema, load_default=None)
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Angebotsposition:
+        """Deserialize JSON to Angebotsposition object"""
+        return Angebotsposition(**data)
diff --git a/src/bo4e/com/standorteigenschaftenallgemein.py b/src/bo4e/com/standorteigenschaftenallgemein.py
index 310a965d9..afa7da568 100644
--- a/src/bo4e/com/standorteigenschaftenallgemein.py
+++ b/src/bo4e/com/standorteigenschaftenallgemein.py
@@ -9,6 +9,7 @@
 
 from bo4e.com.com import COM, COMSchema
 
+
 # pylint: disable=too-few-public-methods
 @attr.s(auto_attribs=True, kw_only=True)
 class StandorteigenschaftenAllgemein(COM):
diff --git a/src/bo4e/com/standorteigenschaftengas.py b/src/bo4e/com/standorteigenschaftengas.py
index a559aaa6a..8a01c7d09 100644
--- a/src/bo4e/com/standorteigenschaftengas.py
+++ b/src/bo4e/com/standorteigenschaftengas.py
@@ -3,6 +3,7 @@
 and corresponding marshmallow schema for de-/serialization
 """
 from typing import List
+
 import attr
 from marshmallow import fields, post_load
 


Test patch:
diff --git a/tests/test_angebotsposition.py b/tests/test_angebotsposition.py
new file mode 100644
index 000000000..5b905ae18
--- /dev/null
+++ b/tests/test_angebotsposition.py
@@ -0,0 +1,54 @@
+from decimal import Decimal
+
+import pytest  # type:ignore[import]
+
+from bo4e.com.angebotsposition import Angebotsposition, AngebotspositionSchema
+from bo4e.com.betrag import Betrag
+from bo4e.com.menge import Menge
+from bo4e.com.preis import Preis
+from bo4e.enum.mengeneinheit import Mengeneinheit
+from bo4e.enum.waehrungscode import Waehrungscode
+from bo4e.enum.waehrungseinheit import Waehrungseinheit
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+
+
+class TestAngebotsposition:
+    @pytest.mark.parametrize(
+        "ap, expected_json_dict",
+        [
+            pytest.param(
+                Angebotsposition(
+                    positionsmenge=Menge(wert=Decimal(4000), einheit=Mengeneinheit.KWH),
+                    positionspreis=Preis(
+                        wert=Decimal(0.2456), einheit=Waehrungseinheit.EUR, bezugswert=Mengeneinheit.KWH
+                    ),
+                    positionskosten=Betrag(
+                        waehrung=Waehrungscode.EUR,
+                        wert=Decimal(98240),
+                    ),
+                    positionsbezeichnung="Beispielangebotsposition",
+                ),
+                {
+                    "positionsbezeichnung": "Beispielangebotsposition",
+                    "positionsmenge": {"wert": "4000", "einheit": "KWH"},
+                    "positionskosten": {"waehrung": "EUR", "wert": "98240"},
+                    "positionspreis": {
+                        "bezugswert": "KWH",
+                        "status": None,
+                        "wert": "0.2456000000000000127453603226967970840632915496826171875",
+                        "einheit": "EUR",
+                    },
+                },
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, ap: Angebotsposition, expected_json_dict: dict):
+        """
+        Test de-/serialisation of Regionskriterium with minimal attributes.
+        """
+        assert_serialization_roundtrip(ap, AngebotspositionSchema(), expected_json_dict)
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Angebotsposition()
+        assert "missing 2 required" in str(excinfo.value)
diff --git a/tests/test_standorteigenschaftengas.py b/tests/test_standorteigenschaftengas.py
index 26ae22eab..e74b4367c 100644
--- a/tests/test_standorteigenschaftengas.py
+++ b/tests/test_standorteigenschaftengas.py
@@ -1,6 +1,6 @@
 import pytest  # type:ignore[import]
-from bo4e.com.marktgebietinfo import MarktgebietInfo
 
+from bo4e.com.marktgebietinfo import MarktgebietInfo
 from bo4e.com.standorteigenschaftengas import StandorteigenschaftenGas, StandorteigenschaftenGasSchema
 from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]