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

Issue:
Implement COM Preisposition
achtung: wurde am 15.12. geändert: 
https://www.bo4e.de/dokumentation/komponenten/com-preisposition/15-12-2021
- #140 
- #87 
- #125 
- #106 
- #84 
- #95 
- #140 


Discussion / hints:

Fix patch:
diff --git a/src/bo4e/com/preisposition.py b/src/bo4e/com/preisposition.py
new file mode 100644
index 000000000..5086159db
--- /dev/null
+++ b/src/bo4e/com/preisposition.py
@@ -0,0 +1,112 @@
+"""
+Contains Preisposition class and corresponding marshmallow schema for de-/serialization
+"""
+from decimal import Decimal
+from typing import List, Optional
+
+import attr
+from marshmallow import fields
+from marshmallow_enum import EnumField  # type:ignore[import]
+
+from bo4e.com.com import COM, COMSchema
+from bo4e.com.preisstaffel import Preisstaffel, PreisstaffelSchema
+from bo4e.enum.artikelid import ArtikelId
+from bo4e.enum.bdewartikelnummer import BDEWArtikelnummer
+from bo4e.enum.bemessungsgroesse import Bemessungsgroesse
+from bo4e.enum.kalkulationsmethode import Kalkulationsmethode
+from bo4e.enum.leistungstyp import Leistungstyp
+from bo4e.enum.mengeneinheit import Mengeneinheit
+from bo4e.enum.tarifzeit import Tarifzeit
+from bo4e.enum.waehrungseinheit import Waehrungseinheit
+from bo4e.enum.zeiteinheit import Zeiteinheit
+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 Preisposition(COM):
+    """
+    Preis für eine definierte Lieferung oder Leistung innerhalb eines Preisblattes
+    """
+
+    # required attributes
+    #: Das Modell, das der Preisbildung zugrunde liegt
+    berechnungsmethode: Kalkulationsmethode = attr.ib(validator=attr.validators.instance_of(Kalkulationsmethode))
+    #: Standardisierte Bezeichnung für die abgerechnete Leistungserbringung
+    leistungstyp: Leistungstyp = attr.ib(validator=attr.validators.instance_of(Leistungstyp))  #
+    #: Bezeichnung für die in der Position abgebildete Leistungserbringung
+    leistungsbezeichnung: str = attr.ib(validator=attr.validators.instance_of(str))
+    #: Festlegung, mit welcher Preiseinheit abgerechnet wird, z.B. Ct. oder €
+    preiseinheit: Waehrungseinheit = attr.ib(validator=attr.validators.instance_of(Waehrungseinheit))
+    #: Hier wird festgelegt, auf welche Bezugsgrösse sich der Preis bezieht, z.B. kWh oder Stück
+    bezugsgroesse: Mengeneinheit = attr.ib(validator=attr.validators.instance_of(Mengeneinheit))
+    #: Preisstaffeln, die zu dieser Preisposition gehören
+    preisstaffeln: List[Preisstaffel] = attr.ib(
+        validator=attr.validators.deep_iterable(
+            member_validator=attr.validators.instance_of(Preisstaffel),
+            iterable_validator=check_list_length_at_least_one,
+        )
+    )
+
+    # optional attributes
+    zeitbasis: Optional[Zeiteinheit] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Zeiteinheit))
+    )
+    """
+    Die Zeit(dauer) auf die sich der Preis bezieht.
+    Z.B. ein Jahr für einen Leistungspreis der in €/kW/Jahr ausgegeben wird
+    """
+    #: Festlegung, für welche Tarifzeit der Preis hier festgelegt ist
+    tarifzeit: Optional[Tarifzeit] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Tarifzeit))
+    )
+    bdew_artikelnummer: Optional[BDEWArtikelnummer] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(BDEWArtikelnummer))
+    )
+    """
+    Eine vom BDEW standardisierte Bezeichnug für die abgerechnete Leistungserbringung;
+    Diese Artikelnummer wird auch im Rechnungsteil der INVOIC verwendet.
+    """
+    #: Mit der Menge der hier angegebenen Größe wird die Staffelung/Zonung durchgeführt. Z.B. Vollbenutzungsstunden
+    zonungsgroesse: Optional[Bemessungsgroesse] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Bemessungsgroesse))
+    )
+    #: Der Anteil der Menge der Blindarbeit in Prozent von der Wirkarbeit, für die keine Abrechnung erfolgt
+    freimenge_blindarbeit: Optional[Decimal] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Decimal))
+    )
+    freimenge_leistungsfaktor: Optional[Decimal] = attr.ib(
+        default=None, validator=attr.validators.optional(attr.validators.instance_of(Decimal))
+    )
+    """
+    Der cos phi (Verhältnis Wirkleistung/Scheinleistung) aus dem die Freimenge für die Blindarbeit berechnet wird als
+    tan phi (Verhältnis Blindleistung/Wirkleistung)
+    """
+    artikel_id: Optional[ArtikelId] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(ArtikelId)), default=None
+    )  #: Standardisierte vom BDEW herausgegebene Liste, welche im Strommarkt die BDEW-Artikelnummer ablöst
+
+
+class PreispositionSchema(COMSchema):
+    """
+    Schema for de-/serialization of Preisposition
+    """
+
+    class_name = Preisposition
+
+    # required attributes
+    berechnungsmethode = EnumField(Kalkulationsmethode)
+    leistungstyp = EnumField(Leistungstyp)
+    leistungsbezeichnung = fields.Str()
+    preiseinheit = EnumField(Waehrungseinheit)
+    bezugsgroesse = EnumField(Mengeneinheit)
+    preisstaffeln = fields.List(fields.Nested(PreisstaffelSchema))
+
+    # optional attributes
+    zeitbasis = EnumField(Zeiteinheit, load_default=None)
+    tarifzeit = EnumField(Tarifzeit, load_default=None)
+    bdew_artikelnummer = EnumField(BDEWArtikelnummer, load_default=None)
+    zonungsgroesse = EnumField(Bemessungsgroesse, load_default=None)
+    freimenge_blindarbeit = fields.Decimal(load_default=None, as_string=True)
+    freimenge_leistungsfaktor = fields.Decimal(load_default=None, as_string=True)
+    artikel_id = EnumField(ArtikelId, load_default=None)
diff --git a/src/bo4e/enum/artikelid.py b/src/bo4e/enum/artikelid.py
new file mode 100644
index 000000000..7c340941d
--- /dev/null
+++ b/src/bo4e/enum/artikelid.py
@@ -0,0 +1,23 @@
+# pylint: disable=missing-module-docstring
+from bo4e.enum.strenum import StrEnum
+
+
+class ArtikelId(StrEnum):
+    """
+    Liste von Artikel-IDs, z.B. für standardisierte vom BDEW herausgegebene Artikel,
+    die im Strommarkt die BDEW-Artikelnummer ablösen
+    """
+
+    # von allen enums ist das hier wohl das mit den unpassendsten Namen ;)
+
+    ARTIKEL_2017001 = "2-01-7-001"  #: Unterbrechung der Anschlussnutzung in der regulären Arbeitszeit (€/Auftrag)
+    ARTIKEL_2017002 = "2-01-7-002"  #: Wiederherstellung der Anschlussnutzung in der regulären Arbeitszeit (€/Auftrag)
+    ARTIKEL_2017003 = "2-01-7-003"  #: Erfolglose Unterbrechung (€/Auftrag)
+    #: Stornierung eines Auftrages zur Unterbrechung der Anschlussnutzung bis zum Vortag der Sperrung (€/Auftrag)
+    ARTIKEL_2017004 = "2-01-7-004"
+    #: Stornierung eines Auftrages zur Unterbrechung der Anschlussnutzung am Tag der Sperrung (€/Auftrag)
+    ARTIKEL_2017005 = "2-01-7-005"
+    #: Wiederherstellung der Anschlussnutzung außerhalb der regulären Arbeitszeit (€/Auftrag)
+    ARTIKEL_2017006 = "2-01-7-006"
+    ARTIKEL_2020001 = "2-02-0-001"  #: Verzugskosten pauschal (€/Fall)
+    ARTIKEL_2020002 = "2-02-0-002"  #: Verzugskosten variabel (€)


Test patch:
diff --git a/tests/test_preisposition.py b/tests/test_preisposition.py
new file mode 100644
index 000000000..85cac762c
--- /dev/null
+++ b/tests/test_preisposition.py
@@ -0,0 +1,51 @@
+from decimal import Decimal
+
+import pytest  # type:ignore[import]
+
+from bo4e.com.preisposition import Preisposition, PreispositionSchema
+from bo4e.enum.artikelid import ArtikelId
+from bo4e.enum.bdewartikelnummer import BDEWArtikelnummer
+from bo4e.enum.bemessungsgroesse import Bemessungsgroesse
+from bo4e.enum.kalkulationsmethode import Kalkulationsmethode
+from bo4e.enum.leistungstyp import Leistungstyp
+from bo4e.enum.mengeneinheit import Mengeneinheit
+from bo4e.enum.tarifzeit import Tarifzeit
+from bo4e.enum.waehrungseinheit import Waehrungseinheit
+from bo4e.enum.zeiteinheit import Zeiteinheit
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+from tests.test_preisstaffel import example_preisstaffel  # type:ignore[import]
+
+
+class TestPreisposition:
+    @pytest.mark.parametrize(
+        "preisposition",
+        [
+            pytest.param(
+                Preisposition(
+                    berechnungsmethode=Kalkulationsmethode.AP_TRANSPORT_ODER_VERTEILNETZ,
+                    leistungstyp=Leistungstyp.SONSTIGER_PREIS,
+                    leistungsbezeichnung="Foo",
+                    preiseinheit=Waehrungseinheit.EUR,
+                    bezugsgroesse=Mengeneinheit.KVARH,
+                    zeitbasis=Zeiteinheit.HALBJAHR,
+                    tarifzeit=Tarifzeit.TZ_HT,
+                    bdew_artikelnummer=BDEWArtikelnummer.AUSGLEICHSENERGIE_UNTERDECKUNG,
+                    zonungsgroesse=Bemessungsgroesse.BENUTZUNGSDAUER,
+                    freimenge_blindarbeit=Decimal(50),  # %
+                    freimenge_leistungsfaktor=Decimal(1.0),
+                    preisstaffeln=[example_preisstaffel],
+                    artikel_id=ArtikelId.ARTIKEL_2017003,
+                )
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, preisposition: Preisposition):
+        """
+        Test de-/serialisation of Preisposition
+        """
+        assert_serialization_roundtrip(preisposition, PreispositionSchema())
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Preisposition()
+        assert "missing 6 required" in str(excinfo.value)
diff --git a/tests/test_preisstaffel.py b/tests/test_preisstaffel.py
index 7ba5fc317..dd8ea6e0e 100644
--- a/tests/test_preisstaffel.py
+++ b/tests/test_preisstaffel.py
@@ -6,6 +6,10 @@
 from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
 from tests.test_sigmoidparameter import example_sigmoidparameter  # type:ignore[import]
 
+example_preisstaffel = Preisstaffel(
+    einheitspreis=Decimal(40.0), staffelgrenze_von=Decimal(12.5), staffelgrenze_bis=Decimal(25.0)
+)
+
 
 class TestPreisstaffel:
     @pytest.mark.parametrize(
@@ -27,9 +31,7 @@ class TestPreisstaffel:
                 id="all attributes",
             ),
             pytest.param(
-                Preisstaffel(
-                    einheitspreis=Decimal(40.0), staffelgrenze_von=Decimal(12.5), staffelgrenze_bis=Decimal(25.0)
-                ),
+                example_preisstaffel,
                 {
                     "einheitspreis": "40",
                     "staffelgrenzeVon": "12.5",