Skip to content

Commit 9bd5734

Browse files
Add cost type NkCostVEWA to report/nk.
1 parent 639ee6c commit 9bd5734

9 files changed

Lines changed: 678 additions & 19 deletions

File tree

django/report/nk/cost/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .base import NkCost, NkCostValueType
22
from .general import NkMonthlyCost, NkPerRentalUnitCost, NkTotalCost, NkTotalEnergyCost
3+
from .vewa import NkCostVEWA
34
from .zev import NkCostZEVStromallmend
45

56
__all__ = [
@@ -10,4 +11,5 @@
1011
"NkMonthlyCost",
1112
"NkTotalEnergyCost",
1213
"NkCostZEVStromallmend",
14+
"NkCostVEWA",
1315
]

django/report/nk/cost/base.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@
99

1010

1111
class NkCostValueType(Enum):
12+
## Default costs / base costs, when costs are split into base costs and usage costs
1213
COST = 1 # The costs that are billed
1314
USAGE = 2 # The usage that is billed (consumed energy, rental unit area, etc.)
1415
WEIGHT = 3 # The (internal) weight for the distribution of the costs
15-
COMMON_COST = (
16-
4 # s Cost from common usage (e.g., Allgemeinstrom) that is split between all rental units
17-
)
18-
COMMON_USAGE = 5
19-
COMMON_WEIGHT = 6
16+
## Usage costs, when costs are split into base costs and usage costs
17+
USAGE_COST = 4 # Usage costs
18+
USAGE_USAGE = 5 # Measured usage
19+
USAGE_WEIGHT = 6
20+
## Costs from common usage (e.g., Allgemeinstrom), that is split between all rental units
21+
COMMON_COST = 7
22+
COMMON_USAGE = 8
23+
COMMON_WEIGHT = 9
2024

2125

2226
@dataclass
@@ -110,9 +114,8 @@ def _normalize_monthly_amounts_for_dict(self, container: dict, value_required=Fa
110114

111115
def split_costs(self):
112116
self._calculate_weights()
113-
for kind in self.total_values:
114-
if kind in (NkCostValueType.COST, NkCostValueType.USAGE):
115-
self._split_cost(kind, NkCostValueType.WEIGHT)
117+
for kind in (NkCostValueType.COST, NkCostValueType.USAGE):
118+
self._split_cost(kind, NkCostValueType.WEIGHT)
116119

117120
def update(self):
118121
pass

django/report/nk/cost/general.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ def __init__(self, report_generator: "NkReportGenerator", cost_config: dict):
2424

2525
def load_input_data(self):
2626
super().load_input_data()
27-
self.total_values[NkCostValueType.COST].amount = self.generator.config.get(
28-
f"Kosten:{self.name}"
29-
)
27+
self.load_building_totals()
3028
self.load_rental_unit_usage()
3129
self.normalize_monthly_amounts()
3230

31+
def load_building_totals(self):
32+
self.total_values[NkCostValueType.COST].amount = self.get_total_costs()
33+
34+
def get_total_costs(self):
35+
return self.generator.config.get(f"Kosten:{self.name}")
36+
3337
def load_rental_unit_usage(self):
3438
for ru in self.generator.rental_units:
3539
weight = getattr(ru, self.rental_unit_usage)

django/report/nk/cost/vewa.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
from collections.abc import Callable
2+
from enum import Enum
3+
from operator import add
4+
from typing import TYPE_CHECKING
5+
6+
from django.utils.translation import gettext_lazy as _
7+
8+
from geno.utils import nformat
9+
10+
from . import NkTotalCost
11+
from .base import NkCommonCostMixin, NkCostValueType, NkMeasurementDataMixin
12+
13+
if TYPE_CHECKING:
14+
from report.nk.contract import NkContract
15+
from report.nk.generator import NkReportGenerator
16+
from report.nk.rental_unit import NkRentalUnit
17+
18+
19+
class NkCostVEWACategories(Enum):
20+
HEAT_WATER = 1 # Warmwasseraufbereitung
21+
HEAT_HEATING = 2 # Heizung
22+
WATER_GENERAL = 3 # Wasser- & Abwasserkosten
23+
24+
25+
class NkCostVEWA(NkCommonCostMixin, NkMeasurementDataMixin, NkTotalCost):
26+
"""VEWA (Verbrauchsabhängige Energie- und Wasserkostenabrechnung, vewa.ch)
27+
28+
Splits between base costs and usage costs (typically 30/70%).
29+
30+
NkCostValueTypes:
31+
- COST, USAGE, WEIGHT: Base costs (handled in NkTotalCost)
32+
- USAGE_COST, USAGE_USAGE, USAGE_WEIGHT: Usage costs
33+
"""
34+
35+
cost_type_id = "vewa"
36+
37+
def __init__(self, report_generator: "NkReportGenerator", cost_config: dict):
38+
super().__init__(report_generator, cost_config)
39+
self.add_value_type(NkCostValueType.USAGE_COST, "Verbrauchsabhängige Kosten", "CHF")
40+
self.add_value_type(NkCostValueType.USAGE_USAGE, "Verbrauch", "kWh")
41+
self.add_value_type(NkCostValueType.COMMON_USAGE, "Allgemeinverbrauch", "kWh")
42+
43+
config = self.generator.config
44+
self.base_cost_factor = float(config.get(cost_config.get("base_cost_factor_key"), 0.3))
45+
self.vewa_category = cost_config.get("vewa_category")
46+
self._validate_config()
47+
48+
def load_building_totals(self):
49+
total_costs = self.get_total_costs()
50+
## Split base costs and usage costs
51+
if isinstance(total_costs, list):
52+
self.total_values[NkCostValueType.COST].monthly_amounts = [
53+
x * self.base_cost_factor for x in total_costs
54+
]
55+
self.total_values[NkCostValueType.USAGE_COST].monthly_amounts = [
56+
x * (1 - self.base_cost_factor) for x in total_costs
57+
]
58+
else:
59+
self.total_values[NkCostValueType.COST].amount = total_costs * self.base_cost_factor
60+
self.total_values[
61+
NkCostValueType.USAGE_COST
62+
].monthly_amounts = self.get_monthly_values_by_building_usage(
63+
total_costs * (1.0 - self.base_cost_factor)
64+
)
65+
66+
# Building-level usage
67+
building_total = self.measurements["building"].get("verbrauch")
68+
if not building_total:
69+
building_total = self.generator.num_months * [0]
70+
elif not isinstance(building_total, list):
71+
# Distribute usage with monthly usage weights
72+
building_total = self.get_monthly_values_by_building_usage(building_total)
73+
self.total_values[NkCostValueType.USAGE_USAGE].monthly_amounts = building_total
74+
75+
# Set common costs from virtual rental unit "allg" (Allgemeinstrom)
76+
# self.set_common_costs(
77+
# self._strom_data[NkVirtualRentalUnitId.COMMON]["chf_total"],
78+
# self._strom_data[NkVirtualRentalUnitId.COMMON]["kwh_total"],
79+
# )
80+
81+
def get_monthly_values_by_building_usage(self, annual_value):
82+
"""Split annual value into monthly values based on monthly usage at the building level."""
83+
monthly_weights = [0] * self.generator.num_months
84+
for ru in self.generator.rental_units:
85+
monthly_weights = list(
86+
map(add, monthly_weights, self.get_rental_unit_usage_weights(ru.id))
87+
)
88+
total_weight = sum(monthly_weights)
89+
monthly_values = []
90+
for i in range(self.generator.num_months):
91+
monthly_values.append(annual_value * monthly_weights[i] / total_weight)
92+
return monthly_values
93+
94+
def get_rental_unit_usage_weights(self, ru_id):
95+
"""Use rental unit measurements as weights to disribute the building totals."""
96+
ru = self.generator.get_rental_unit_by_id(ru_id)
97+
ru_messung = self.measurements["rental_units"].get(ru.name, {})
98+
return ru_messung.get("verbrauch", self.generator.num_months * [0.0])
99+
100+
def split_costs(self):
101+
# Base costs are handled by the super class
102+
super().split_costs()
103+
# Split usage costs
104+
self._calculate_usage_weights()
105+
for kind in (NkCostValueType.USAGE_COST, NkCostValueType.USAGE_USAGE):
106+
self._split_cost(kind, NkCostValueType.USAGE_WEIGHT)
107+
# self._calculate_weights()
108+
# self._split_common_costs()
109+
# self._aggregate_monthly_amounts()
110+
111+
def _calculate_usage_weights(self):
112+
self._calculate_weights_for_type(
113+
NkCostValueType.USAGE_WEIGHT, "get_rental_unit_usage_weights"
114+
)
115+
116+
def get_extra_context(self, ru: "NkRentalUnit", contract: "NkContract") -> dict:
117+
"""Return Stromkosten detail variables for the ODT bill template."""
118+
119+
ru_data = self._strom_data.get(ru.id, self._zero_strom_data(self.generator.num_months))
120+
d = self.get_assigned_amounts(ru_data, contract, ru)
121+
bt = self._building_totals
122+
123+
# Common costs (Allgemeinstrom)
124+
common_cost = self._get_assigned_amount(NkCostValueType.COMMON_COST, contract, ru)
125+
common_weight = self._get_assigned_amount(NkCostValueType.COMMON_WEIGHT, contract, ru)
126+
common_total_cost = self.total_values[NkCostValueType.COMMON_COST].amount
127+
common_total_weight = self.total_values[NkCostValueType.COMMON_WEIGHT].amount
128+
129+
def fmt(val):
130+
return nformat(val)
131+
132+
def fmt_kwh(val):
133+
return nformat(val, 0)
134+
135+
def rate(chf, kwh):
136+
return nformat(chf / kwh if kwh else 0, 4)
137+
138+
# Building totals (formatted)
139+
ctx = {
140+
# Eigenverbrauch Solar direkt (from roof)
141+
"ssd_chft": fmt(bt["ssd"]["chf"]),
142+
"ssdt": fmt_kwh(bt["ssd"]["kwh"]),
143+
"ssd_eh": rate(bt["ssd"]["chf"], bt["ssd"]["kwh"]),
144+
"ssd": fmt_kwh(d["kwh_solar"]),
145+
"ssd_chf": fmt(d["chf_solar_eigen"]),
146+
# Eigenverbrauch Solar via Speicher/Stromallmend
147+
"sss_chft": fmt(bt["sss"]["chf"]),
148+
"ssst": fmt_kwh(bt["sss"]["kwh"]),
149+
"sss_eh": rate(bt["sss"]["chf"], bt["sss"]["kwh"]),
150+
"sss": fmt_kwh(d["kwh_solar_speicher"]),
151+
"sss_chf": fmt(d["chf_solar_speicher"]),
152+
# Netzstrombezug Hochtarif
153+
"snh_chft": fmt(bt["snh"]["chf"]),
154+
"snht": fmt_kwh(bt["snh"]["kwh"]),
155+
"snh_eh": rate(bt["snh"]["chf"], bt["snh"]["kwh"]),
156+
"snh": fmt_kwh(d["kwh_netz_hoch"]),
157+
"snh_chf": fmt(d["chf_netz_hoch"]),
158+
# Netzstrombezug Niedertarif
159+
"snt_chft": fmt(bt["snt"]["chf"]),
160+
"sntt": fmt_kwh(bt["snt"]["kwh"]),
161+
"snt_eh": rate(bt["snt"]["chf"], bt["snt"]["kwh"]),
162+
"snt": fmt_kwh(d["kwh_netz_nieder"]),
163+
"snt_chf": fmt(d["chf_netz_nieder"]),
164+
# Herkunftsnachweise (HKN)
165+
"shk_chft": fmt(bt["shk"]["chf"]),
166+
"shkt": fmt_kwh(bt["shk"]["kwh"]),
167+
"shk_eh": rate(bt["shk"]["chf"], bt["shk"]["kwh"]),
168+
"shk": fmt_kwh(d["kwh_solar_einkauf"]),
169+
"shk_chf": fmt(d["chf_solar_hkn"]),
170+
# Korrektur
171+
"sk_chft": fmt(bt["sk"]["chf"]),
172+
"skt": fmt_kwh(bt["sk"]["kwh"]),
173+
"sk_eh": rate(bt["sk"]["chf"], bt["sk"]["kwh"]),
174+
"sk": fmt_kwh(d["kwh_korrektur"]),
175+
"sk_chf": fmt(d["chf_korrektur"]),
176+
# Strom subtotal ( of above, no separate Allgemeinstrom/fees in this class)
177+
"st_chft": fmt(bt["total"]["chf"]),
178+
"stt": fmt_kwh(bt["total"]["kwh"]),
179+
"st": fmt_kwh(d["kwh_total"]),
180+
"st_chf": fmt(d["chf_total"]),
181+
# Anteil Allgemeinstrom (not computed by this class – leave empty)
182+
"sa_chft": fmt(common_total_cost),
183+
"sat": nformat(common_total_weight, 0),
184+
"sa_eh": nformat(
185+
common_total_cost / common_total_weight if common_total_weight else 0, 2
186+
),
187+
"sa": nformat(common_weight, 1),
188+
"sa_chf": fmt(common_cost),
189+
# Stromnebenkosten/Messung (not computed by this class – leave empty)
190+
"snk_chft": "",
191+
"snkt": "",
192+
"snk_eh": "",
193+
"snk": "",
194+
"snk_chf": "",
195+
# Grand total, building totals already include common costs
196+
"stot_chft": fmt(bt["total"]["chf"]),
197+
"stot_chf": fmt(d["chf_total"] + common_cost),
198+
}
199+
return ctx
200+
201+
def get_export_extra_info(
202+
self, include_percent: bool = False, formatter: Callable = lambda x: x
203+
) -> list:
204+
lines = []
205+
for key in (
206+
"kwh_solar", # "messung_strom_solar",
207+
"kwh_solar_speicher",
208+
"kwh_solar_einkauf",
209+
"kwh_netzstrom",
210+
"kwh_netz_hoch",
211+
"kwh_netz_nieder",
212+
"kwh_korrektur",
213+
"kwh_total",
214+
"chf_solar_eigen",
215+
"chf_solar_speicher", # "chf_solar_einspeise",
216+
"chf_solar_hkn",
217+
"chf_netz_hoch", # "messung_chf_netz_hoch",
218+
"chf_netz_nieder", # "messung_chf_netz_nieder",
219+
"chf_korrektur",
220+
"chf_total",
221+
):
222+
obj_data = []
223+
total = 0
224+
for ru in self.generator.rental_units:
225+
d = self._strom_data.get(ru.id, self._zero_strom_data(self.generator.num_months))
226+
value = d.get(key)
227+
if value:
228+
annual_value = sum(value)
229+
total += annual_value
230+
obj_data.append(formatter(annual_value))
231+
else:
232+
obj_data.append("")
233+
if include_percent:
234+
obj_data.append("")
235+
236+
row = [key, formatter(total)]
237+
for _s in self.generator.sections:
238+
row.append("")
239+
if include_percent:
240+
row.append("")
241+
row += obj_data
242+
lines.append(row)
243+
return lines
244+
245+
@staticmethod
246+
def _zero_data(num_months: int) -> dict:
247+
zeros = num_months * [0.0]
248+
return {
249+
"verbrauch": list(zeros),
250+
}
251+
252+
def _validate_config(self):
253+
if self.base_cost_factor < 0.0 or self.base_cost_factor > 1.0:
254+
raise ValueError(
255+
_("The base cost factor is not in the range 0.0 - 1.0: {factor}").format(
256+
tarif=self.base_cost_factor
257+
)
258+
)
259+
if not isinstance(self.vewa_category, NkCostVEWACategories):
260+
raise ValueError(
261+
_("Invalid VEWA category: {category}").format(category=self.vewa_category)
262+
)

django/report/nk/cost_config.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from dataclasses import dataclass
22

33
from report.nk.cost import NkCost, NkCostZEVStromallmend, NkPerRentalUnitCost, NkTotalCost
4+
from report.nk.cost.vewa import NkCostVEWA, NkCostVEWACategories
45
from report.nk.measurement_data import (
6+
NkMeasurementDataAnnual,
57
NkMeasurementDataEgon,
68
NkMeasurementDataMonthly,
79
)
@@ -49,6 +51,29 @@ def get_costs_from_config():
4951
"billing_group": "Kehrichtgebühren",
5052
"class": NkTotalCost,
5153
},
54+
{
55+
"class": NkCostVEWA,
56+
"name": "Wasser_Abwasser",
57+
"billing_group": "Wasserkosten",
58+
"vewa_category": NkCostVEWACategories.WATER_GENERAL,
59+
"base_cost_factor_key": "Wasserkosten:Grundkostenanteil",
60+
"measurement_data": {
61+
"building": {
62+
"class": NkMeasurementDataAnnual,
63+
"value_key": "Messdaten:Wasserverbrauch",
64+
},
65+
"rental_units": {
66+
"class": NkMeasurementDataEgon,
67+
"file_key": "Messdaten:Mieteinheiten",
68+
"file_prefix": "egon_Waerme",
69+
"headers": {
70+
"rental_unit": "Gebäudeeinheit",
71+
"time_period": "Mieter Abrechnungsperiode",
72+
"verbrauch": "Warmwasser Verbrauch (Kubikmeter)",
73+
},
74+
},
75+
},
76+
},
5277
{
5378
"name": "Wasser_Abwasser_Grundkosten",
5479
"category": "waerme_wasser_grund",

0 commit comments

Comments
 (0)