Skip to content

Commit 23715f0

Browse files
✨Implement COM Angebotsteil (#284)
* ✨Implement COM Angebotsteil * change twoline docstring to correct syntax Co-authored-by: Kevin <68426071+hf-krechan@users.noreply.github.com>
1 parent 8a6d186 commit 23715f0

3 files changed

Lines changed: 289 additions & 0 deletions

File tree

docs/api/bo4e.com.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ bo4e.com.angebotsposition module
2020
:undoc-members:
2121
:show-inheritance:
2222

23+
bo4e.com.angebotsteil module
24+
----------------------------
25+
26+
.. automodule:: bo4e.com.angebotsteil
27+
:members:
28+
:undoc-members:
29+
:show-inheritance:
30+
2331
bo4e.com.aufabschlag module
2432
---------------------------
2533

src/bo4e/com/angebotsteil.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Contains Angebotsteil class
3+
and corresponding marshmallow schema for de-/serialization
4+
"""
5+
6+
from typing import List, Optional
7+
8+
import attr
9+
from marshmallow import fields, post_load
10+
11+
from bo4e.bo.marktlokation import Marktlokation, MarktlokationSchema
12+
from bo4e.com.angebotsposition import Angebotsposition, AngebotspositionSchema
13+
from bo4e.com.betrag import Betrag, BetragSchema
14+
from bo4e.com.com import COM, COMSchema
15+
from bo4e.com.menge import Menge, MengeSchema
16+
from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
17+
from bo4e.validators import check_list_length_at_least_one
18+
19+
20+
# pylint: disable=too-few-public-methods
21+
@attr.s(auto_attribs=True, kw_only=True)
22+
class Angebotsteil(COM):
23+
"""
24+
Mit dieser Komponente wird ein Teil einer Angebotsvariante abgebildet.
25+
Hier werden alle Angebotspositionen aggregiert.
26+
Angebotsteile werden im einfachsten Fall für eine Marktlokation oder Lieferstellenadresse erzeugt.
27+
Hier werden die Mengen und Gesamtkosten aller Angebotspositionen zusammengefasst.
28+
Eine Variante besteht mindestens aus einem Angebotsteil.
29+
"""
30+
31+
# required attributes
32+
#: Einzelne Positionen, die zu diesem Angebotsteil gehören
33+
positionen: List[Angebotsposition] = attr.ib(
34+
validator=[
35+
attr.validators.deep_iterable(
36+
member_validator=attr.validators.instance_of(Angebotsposition),
37+
iterable_validator=attr.validators.instance_of(list),
38+
),
39+
check_list_length_at_least_one,
40+
]
41+
)
42+
43+
# optional attributes
44+
#: Identifizierung eines Subkapitels einer Anfrage, beispielsweise das Los einer Ausschreibung
45+
anfrage_subreferenz: Optional[str] = attr.ib(
46+
default=None, validator=attr.validators.optional(attr.validators.instance_of(str))
47+
)
48+
lieferstellenangebotsteil: Optional[List[Marktlokation]] = attr.ib(
49+
default=None,
50+
validator=attr.validators.optional(
51+
attr.validators.deep_iterable(
52+
member_validator=attr.validators.instance_of(Marktlokation),
53+
iterable_validator=attr.validators.instance_of(list),
54+
)
55+
),
56+
)
57+
"""
58+
Marktlokationen, für die dieses Angebotsteil gilt, falls vorhanden.
59+
Durch die Marktlokation ist auch die Lieferadresse festgelegt
60+
"""
61+
#: Summe der Verbräuche aller in diesem Angebotsteil eingeschlossenen Lieferstellen
62+
gesamtmengeangebotsteil: Optional[Menge] = attr.ib(
63+
default=None, validator=attr.validators.optional(attr.validators.instance_of(Menge))
64+
)
65+
#: Summe der Jahresenergiekosten aller in diesem Angebotsteil enthaltenen Lieferstellen
66+
gesamtkostenangebotsteil: Optional[Betrag] = attr.ib(
67+
default=None, validator=attr.validators.optional(attr.validators.instance_of(Betrag))
68+
)
69+
#: Hier kann der Belieferungszeitraum angegeben werden, für den dieser Angebotsteil gilt
70+
lieferzeitraum: Optional[Zeitraum] = attr.ib(
71+
default=None, validator=attr.validators.optional(attr.validators.instance_of(Zeitraum))
72+
)
73+
74+
75+
class AngebotsteilSchema(COMSchema):
76+
"""
77+
Schema for de-/serialization of Angebotsteil.
78+
"""
79+
80+
# required attributes
81+
positionen = fields.List(fields.Nested(AngebotspositionSchema))
82+
83+
# optional attributes
84+
anfrage_subreferenz = fields.Str(load_default=None)
85+
lieferstellenangebotsteil = fields.List(fields.Nested(MarktlokationSchema), load_default=None)
86+
gesamtmengeangebotsteil = fields.Nested(MengeSchema, load_default=None)
87+
gesamtkostenangebotsteil = fields.Nested(BetragSchema, load_default=None)
88+
lieferzeitraum = fields.Nested(ZeitraumSchema, load_default=None)
89+
90+
# pylint: disable=no-self-use, unused-argument
91+
@post_load
92+
def deserialize(self, data, **kwargs) -> Angebotsteil:
93+
"""Deserialize JSON to Angebotsteil object"""
94+
return Angebotsteil(**data)

tests/test_angebotsteil.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
from datetime import datetime, timezone
2+
from decimal import Decimal
3+
4+
import pytest # type:ignore[import]
5+
6+
from bo4e.bo.marktlokation import Marktlokation
7+
from bo4e.com.adresse import Adresse
8+
from bo4e.com.angebotsposition import Angebotsposition
9+
from bo4e.com.angebotsteil import Angebotsteil, AngebotsteilSchema
10+
from bo4e.com.betrag import Betrag
11+
from bo4e.com.menge import Menge
12+
from bo4e.com.preis import Preis
13+
from bo4e.com.zeitraum import Zeitraum
14+
from bo4e.enum.bilanzierungsmethode import Bilanzierungsmethode
15+
from bo4e.enum.energierichtung import Energierichtung
16+
from bo4e.enum.mengeneinheit import Mengeneinheit
17+
from bo4e.enum.netzebene import Netzebene
18+
from bo4e.enum.sparte import Sparte
19+
from bo4e.enum.waehrungscode import Waehrungscode
20+
from bo4e.enum.waehrungseinheit import Waehrungseinheit
21+
from tests.serialization_helper import assert_serialization_roundtrip # type:ignore[import]
22+
23+
24+
class TestAngebotsteil:
25+
@pytest.mark.parametrize(
26+
"angebotsteil, expected_json_dict",
27+
[
28+
pytest.param(
29+
Angebotsteil(
30+
positionen=[
31+
Angebotsposition(
32+
positionsbezeichnung="testtring",
33+
positionsmenge=Menge(wert=Decimal(4000), einheit=Mengeneinheit.KWH),
34+
positionspreis=Preis(
35+
wert=Decimal(0.2456), einheit=Waehrungseinheit.EUR, bezugswert=Mengeneinheit.KWH
36+
),
37+
positionskosten=Betrag(
38+
waehrung=Waehrungscode.EUR,
39+
wert=Decimal(98240),
40+
),
41+
)
42+
],
43+
anfrage_subreferenz="teststring",
44+
lieferstellenangebotsteil=[
45+
Marktlokation(
46+
marktlokations_id="51238696781",
47+
sparte=Sparte.GAS,
48+
lokationsadresse=Adresse(
49+
postleitzahl="82031",
50+
ort="Grünwald",
51+
hausnummer="27A",
52+
strasse="Nördliche Münchner Straße",
53+
),
54+
energierichtung=Energierichtung.EINSP,
55+
bilanzierungsmethode=Bilanzierungsmethode.PAUSCHAL,
56+
unterbrechbar=True,
57+
netzebene=Netzebene.NSP,
58+
)
59+
],
60+
gesamtmengeangebotsteil=Menge(wert=Decimal(4000), einheit=Mengeneinheit.KWH),
61+
gesamtkostenangebotsteil=Betrag(
62+
waehrung=Waehrungscode.EUR,
63+
wert=Decimal(98240),
64+
),
65+
lieferzeitraum=Zeitraum(
66+
startdatum=datetime(2020, 1, 1, tzinfo=timezone.utc),
67+
enddatum=datetime(2020, 4, 1, tzinfo=timezone.utc),
68+
),
69+
),
70+
{
71+
"positionen": [
72+
{
73+
"positionsbezeichnung": "testtring",
74+
"positionsmenge": {"wert": "4000", "einheit": "KWH"},
75+
"positionskosten": {"waehrung": "EUR", "wert": "98240"},
76+
"positionspreis": {
77+
"bezugswert": "KWH",
78+
"status": None,
79+
"wert": "0.2456000000000000127453603226967970840632915496826171875",
80+
"einheit": "EUR",
81+
},
82+
},
83+
],
84+
"lieferstellenangebotsteil": [
85+
{
86+
"marktlokationsId": "51238696781",
87+
"sparte": "GAS",
88+
"lokationsadresse": {
89+
"postleitzahl": "82031",
90+
"ort": "Grünwald",
91+
"hausnummer": "27A",
92+
"strasse": "Nördliche Münchner Straße",
93+
"adresszusatz": None,
94+
"postfach": None,
95+
"coErgaenzung": None,
96+
"landescode": "DE",
97+
},
98+
"energierichtung": "EINSP",
99+
"bilanzierungsmethode": "PAUSCHAL",
100+
"unterbrechbar": True,
101+
"netzebene": "NSP",
102+
"netzgebietsnr": None,
103+
"versionstruktur": "2",
104+
"katasterinformation": None,
105+
"bilanzierungsgebiet": None,
106+
"grundversorgercodenr": None,
107+
"endkunde": None,
108+
"geoadresse": None,
109+
"verbrauchsart": None,
110+
"netzbetreibercodenr": None,
111+
"gebietstyp": None,
112+
"gasqualitaet": None,
113+
"zugehoerigeMesslokation": None,
114+
"externeReferenzen": [],
115+
"boTyp": "MARKTLOKATION",
116+
}
117+
],
118+
"gesamtmengeangebotsteil": {"wert": "4000", "einheit": "KWH"},
119+
"gesamtkostenangebotsteil": {"waehrung": "EUR", "wert": "98240"},
120+
"anfrageSubreferenz": "teststring",
121+
"lieferzeitraum": {
122+
"startdatum": "2020-01-01T00:00:00+00:00",
123+
"endzeitpunkt": None,
124+
"einheit": None,
125+
"enddatum": "2020-04-01T00:00:00+00:00",
126+
"startzeitpunkt": None,
127+
"dauer": None,
128+
},
129+
},
130+
id="maximal attributes",
131+
),
132+
pytest.param(
133+
Angebotsteil(
134+
positionen=[
135+
Angebotsposition(
136+
positionsbezeichnung="teststring",
137+
positionsmenge=Menge(wert=Decimal(4000), einheit=Mengeneinheit.KWH),
138+
positionspreis=Preis(
139+
wert=Decimal(0.2456), einheit=Waehrungseinheit.EUR, bezugswert=Mengeneinheit.KWH
140+
),
141+
positionskosten=Betrag(
142+
waehrung=Waehrungscode.EUR,
143+
wert=Decimal(98240),
144+
),
145+
)
146+
],
147+
),
148+
{
149+
"positionen": [
150+
{
151+
"positionsbezeichnung": "teststring",
152+
"positionsmenge": {"wert": "4000", "einheit": "KWH"},
153+
"positionskosten": {"waehrung": "EUR", "wert": "98240"},
154+
"positionspreis": {
155+
"bezugswert": "KWH",
156+
"status": None,
157+
"wert": "0.2456000000000000127453603226967970840632915496826171875",
158+
"einheit": "EUR",
159+
},
160+
},
161+
],
162+
"anfrageSubreferenz": None,
163+
"lieferstellenangebotsteil": None,
164+
"gesamtmengeangebotsteil": None,
165+
"gesamtkostenangebotsteil": None,
166+
"lieferzeitraum": None,
167+
},
168+
id="minimal attributes",
169+
),
170+
],
171+
)
172+
def test_serialization_roundtrip(self, angebotsteil, expected_json_dict):
173+
"""
174+
Test de-/serialisation of Angebotsteil with minimal attributes.
175+
"""
176+
assert_serialization_roundtrip(angebotsteil, AngebotsteilSchema(), expected_json_dict)
177+
178+
def test_angebotsteil_positionen_required(self):
179+
with pytest.raises(ValueError) as excinfo:
180+
_ = Angebotsteil(positionen=[])
181+
182+
assert "List positionen must not be empty." in str(excinfo.value)
183+
184+
def test_missing_required_attribute(self):
185+
with pytest.raises(TypeError) as excinfo:
186+
_ = Angebotsteil()
187+
assert "missing 1 required" in str(excinfo.value)

0 commit comments

Comments
 (0)