Skip to content

Commit 93789c9

Browse files
Add support for monthly costs from measurement data.
1 parent fd7b7a0 commit 93789c9

8 files changed

Lines changed: 349 additions & 142 deletions

File tree

django/report/nk/bill.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ def _get_rental_unit_context(
103103
context["costs"].append(cost_context)
104104
aggregated_values = {}
105105
for cost in costs:
106-
# context.update(cost.get_extra_context(ru, self.contract))
107106
cost.update_context(ru, self.contract, context, aggregated_values)
108107
return context
109108

django/report/nk/cost/base.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def __init__(self, report_generator: "NkReportGenerator", cost_config: dict):
5050
self.rental_unit_values: dict[int, dict[NkCostValueType, NkCostValue]] = {}
5151
self.section_weights = cost_config.get("section_weights", "default")
5252
self.add_value_type(NkCostValueType.COST, "Kosten", "CHF")
53+
self.warnings = []
5354

5455
def add_value_type(self, kind: NkCostValueType, name: str, unit: str):
5556
self._add_value_type_to_dict(self.total_values, kind, name, unit)
@@ -115,7 +116,8 @@ def _normalize_monthly_amounts_for_dict(self, container: dict, value_required=Fa
115116
def split_costs(self):
116117
self._calculate_weights()
117118
for kind in (NkCostValueType.COST, NkCostValueType.USAGE):
118-
self._split_cost(kind, NkCostValueType.WEIGHT)
119+
if kind in self.total_values:
120+
self._split_cost(kind, NkCostValueType.WEIGHT)
119121

120122
def update(self):
121123
pass
@@ -151,8 +153,8 @@ def _calculate_amounts(
151153
and abs(values[kind].monthly_amounts[month] - amount) > 0.01
152154
and kind != NkCostValueType.USAGE
153155
):
154-
print(
155-
"WARNING: overwriting existing monthly amount for "
156+
self.add_warning(
157+
"overwriting existing monthly amount for "
156158
f"{self.name} {kind}/{month}: "
157159
f"{values[kind].monthly_amounts[month]} => {amount}"
158160
)
@@ -163,7 +165,10 @@ def _calculate_amounts(
163165
and abs(values[kind].amount - total_amount) > 0.01
164166
and kind != NkCostValueType.USAGE
165167
):
166-
print(f"WARNING: overwriting existing amount: {values[kind].amount} => {total_amount}")
168+
self.add_warning(
169+
f"overwriting existing amount: {values[kind].amount} => {total_amount}"
170+
)
171+
167172
values[kind].amount = total_amount
168173

169174
def _calculate_weights(self):
@@ -180,7 +185,7 @@ def _calculate_weights_for_type(
180185
if not callable(rental_unit_weights_function):
181186
raise ValueError(f"Invalid function name: {rental_unit_weights_function_name}")
182187
for ru in self.generator.rental_units:
183-
ru_weights = rental_unit_weights_function(ru.id)
188+
ru_weights = rental_unit_weights_function(ru)
184189
values = self.rental_unit_values[ru.id][value_type]
185190
section = self.section_values[ru.section.id][value_type]
186191
for month in range(self.generator.num_months):
@@ -230,9 +235,8 @@ def get_section_weights(self):
230235
weights[section.id] = 1.0
231236
return weights
232237

233-
def get_rental_unit_weights(self, ru_id):
238+
def get_rental_unit_weights(self, ru):
234239
"""Default with equal weights for all rental units."""
235-
ru = self.generator.get_rental_unit_by_id(ru_id)
236240
if ru.is_virtual:
237241
return self.generator.num_months * [0.0]
238242
else:
@@ -316,6 +320,10 @@ def update_context(
316320
) -> None:
317321
context.update(self._get_context(ru, contract))
318322

323+
def add_warning(self, msg):
324+
print(f"WARNING: {msg}")
325+
self.warnings.append(msg)
326+
319327

320328
class NkMeasurementDataMixin:
321329
"""Mixin for NkCosts that require measurement data."""
@@ -349,9 +357,8 @@ def get_assigned_cost(self, contract: "NkContract", rental_unit: "NkRentalUnit |
349357
ret = super().get_assigned_cost(contract, rental_unit)
350358
return ret + self._get_assigned_amount(NkCostValueType.COMMON_COST, contract, rental_unit)
351359

352-
def get_rental_unit_common_weights(self, ru_id):
360+
def get_rental_unit_common_weights(self, ru):
353361
"""Default is the rental unit area (per period)."""
354-
ru = self.generator.get_rental_unit_by_id(ru_id)
355362
if ru.is_virtual:
356363
return self.generator.num_months * [0.0]
357364
else:

django/report/nk/cost/general.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,17 @@ def get_total_costs(self):
3636

3737
def load_rental_unit_usage(self):
3838
for ru in self.generator.rental_units:
39-
weight = getattr(ru, self.rental_unit_usage)
39+
weight = sum(self.get_rental_unit_weights(ru))
4040
self.rental_unit_values[ru.id][NkCostValueType.USAGE].amount = weight
4141
self.section_values[ru.section.id][NkCostValueType.USAGE].amount += weight
4242
self.total_values[NkCostValueType.USAGE].amount += weight
4343

44-
def get_rental_unit_weights(self, ru_id):
44+
def get_rental_unit_weights(self, ru):
4545
"""Use the usage as weight."""
46-
return self.rental_unit_values[ru_id][NkCostValueType.USAGE].monthly_amounts
46+
return [
47+
getattr(ru, self.rental_unit_usage) / self.generator.num_months
48+
] * self.generator.num_months
49+
# return self.rental_unit_values[ru.id][NkCostValueType.USAGE].monthly_amounts
4750

4851

4952
class NkMonthlyCost(NkCost):

django/report/nk/cost/vewa.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def __init__(self, report_generator: "NkReportGenerator", cost_config: dict):
4848

4949
def load_building_totals(self):
5050
total_costs = self.get_total_costs()
51+
if total_costs is None:
52+
raise ValueError(_("No total costs found for {cost_name}").format(cost_name=self.name))
5153
## Split base costs and usage costs
5254
if isinstance(total_costs, list):
5355
self.total_values[NkCostValueType.COST].monthly_amounts = [
@@ -65,9 +67,9 @@ def load_building_totals(self):
6567
)
6668

6769
# Building-level usage
68-
building_total = self.measurements["building"].get("verbrauch")
70+
building_total = self.measurements["building"].get("usage")
6971
if not building_total:
70-
building_total = self.generator.num_months * [0]
72+
building_total = self._get_missing_building_usage()
7173
elif not isinstance(building_total, list):
7274
# Distribute usage with monthly usage weights
7375
building_total = self.get_monthly_values_by_building_usage(building_total)
@@ -79,32 +81,48 @@ def load_building_totals(self):
7981
# self._strom_data[NkVirtualRentalUnitId.COMMON]["kwh_total"],
8082
# )
8183

84+
def get_total_costs(self):
85+
# Try to get the costs from the building measurements
86+
total_costs = self.measurements["building"].get("costs")
87+
if isinstance(total_costs, list):
88+
return total_costs
89+
return super().get_total_costs()
90+
91+
def _get_missing_building_usage(self):
92+
"""Try to get the building usage from the rental unit measurements."""
93+
building_usage = self.generator.num_months * [0]
94+
for ru in self.generator.rental_units:
95+
usage = self.measurements["rental_units"].get(ru.name, {}).get("usage")
96+
if usage:
97+
building_usage = list(map(add, building_usage, usage))
98+
return building_usage
99+
82100
def get_monthly_values_by_building_usage(self, annual_value):
83101
"""Split annual value into monthly values based on monthly usage at the building level."""
84102
monthly_weights = [0] * self.generator.num_months
85103
for ru in self.generator.rental_units:
86104
monthly_weights = list(
87-
map(add, monthly_weights, self.get_rental_unit_usage_weights(ru.id))
105+
map(add, monthly_weights, self.get_rental_unit_usage_weights(ru))
88106
)
89107
total_weight = sum(monthly_weights)
90108
monthly_values = []
91109
for i in range(self.generator.num_months):
92110
monthly_values.append(annual_value * monthly_weights[i] / total_weight)
93111
return monthly_values
94112

95-
def get_rental_unit_usage_weights(self, ru_id):
113+
def get_rental_unit_usage_weights(self, ru):
96114
"""Use rental unit measurements as weights to disribute the building totals."""
97-
ru = self.generator.get_rental_unit_by_id(ru_id)
98115
ru_messung = self.measurements["rental_units"].get(ru.name, {})
99-
return ru_messung.get("verbrauch", self.generator.num_months * [0.0])
116+
return ru_messung.get("usage", self.generator.num_months * [0.0])
100117

101-
def get_rental_unit_weights(self, ru_id):
102-
if self.exclude_zero_usage_units and self._has_zero_usage(ru_id):
103-
return 0
104-
return super().get_rental_unit_weights(ru_id)
118+
def get_rental_unit_weights(self, ru):
119+
if self.exclude_zero_usage_units and self._has_zero_usage(ru):
120+
self.add_warning(f"Excluding rental unit with zero usage: {ru.name}/{ru.id}")
121+
return self.generator.num_months * [0.0]
122+
return super().get_rental_unit_weights(ru)
105123

106-
def _has_zero_usage(self, ru_id):
107-
return self.measurements["rental_units"].get(ru_id, {}).get("verbrauch", 0) == 0
124+
def _has_zero_usage(self, ru):
125+
return sum(self.measurements["rental_units"].get(ru.name, {}).get("usage", [0])) == 0
108126

109127
def split_costs(self):
110128
# Base costs are handled by the super class
@@ -356,7 +374,7 @@ def get_export_extra_info(
356374
def _zero_data(num_months: int) -> dict:
357375
zeros = num_months * [0.0]
358376
return {
359-
"verbrauch": list(zeros),
377+
"usage": list(zeros),
360378
}
361379

362380
def _validate_config(self):

django/report/nk/cost_config.py

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
from dataclasses import dataclass
22
from enum import Enum
33

4-
from report.nk.cost import NkCost, NkCostZEVStromallmend, NkPerRentalUnitCost, NkTotalCost
5-
from report.nk.cost.vewa import NkCostVEWA, NkCostVEWACategories
4+
from report.nk.cost import (
5+
NkCost,
6+
NkCostVEWA,
7+
NkCostZEVStromallmend,
8+
NkPerRentalUnitCost,
9+
NkTotalCost,
10+
)
11+
from report.nk.cost.vewa import NkCostVEWACategories
612
from report.nk.measurement_data import (
713
NkMeasurementDataAnnual,
814
NkMeasurementDataBase,
915
NkMeasurementDataEgon,
10-
NkMeasurementDataMonthly,
16+
NkMeasurementDataMonthlyCSVFile,
1117
)
1218

1319

@@ -33,7 +39,7 @@ class CostConfigField:
3339
class CostConfigMeasurementSourceField:
3440
key: str
3541
supported_sources: list[type[NkMeasurementDataBase]]
36-
required_keys: list[str]
42+
keys: list[str]
3743

3844

3945
@dataclass
@@ -85,13 +91,13 @@ def get_fields(cls):
8591
subfields=[
8692
CostConfigMeasurementSourceField(
8793
"building",
88-
supported_sources=[NkMeasurementDataMonthly],
89-
required_keys=["strom_bezug_zev", "strom_ruecklieferung_ew"],
94+
supported_sources=[NkMeasurementDataMonthlyCSVFile],
95+
keys=["strom_bezug_zev", "strom_ruecklieferung_ew"],
9096
),
9197
CostConfigMeasurementSourceField(
9298
"rental_units",
9399
supported_sources=[NkMeasurementDataEgon],
94-
required_keys=[
100+
keys=[
95101
"strom_ew_nieder",
96102
"strom_ew_hoch",
97103
"strom_solar",
@@ -105,7 +111,7 @@ def get_fields(cls):
105111
# Example generated measurement sources config:
106112
#
107113
# "building": {
108-
# "class": NkMeasurementDataMonthly,
114+
# "class": NkMeasurementDataMonthlyCSVFile,
109115
# "file_key": "Messdaten:Liegenschaft",
110116
# "headers": {
111117
# "month": "Monat",
@@ -143,12 +149,12 @@ def get_fields(cls):
143149
CostConfigMeasurementSourceField(
144150
"building",
145151
supported_sources=[NkMeasurementDataAnnual],
146-
required_keys=["verbrauch"],
152+
keys=["usage", "costs"],
147153
),
148154
CostConfigMeasurementSourceField(
149155
"rental_units",
150156
supported_sources=[NkMeasurementDataEgon],
151-
required_keys=["verbrauch"],
157+
keys=["usage"],
152158
),
153159
],
154160
),
@@ -173,7 +179,7 @@ def get_fields(cls):
173179
# "headers": {
174180
# "rental_unit": "Gebäudeeinheit",
175181
# "time_period": "Mieter Abrechnungsperiode",
176-
# "verbrauch": "Warmwasser Verbrauch (Kubikmeter)",
182+
# "usage": "Warmwasser Verbrauch (Kubikmeter)",
177183
# },
178184
# },
179185
# },
@@ -220,7 +226,7 @@ def get_fields(cls):
220226
# Example generated measurement sources config:
221227
#
222228
# "building": {
223-
# "class": NkMeasurementDataMonthly,
229+
# "class": NkMeasurementDataMonthlyCSVFile,
224230
# "file_key": "Messdaten:Liegenschaft",
225231
# "headers": {
226232
# "month": "Monat",
@@ -253,7 +259,7 @@ def get_fields(cls):
253259
# "headers": {
254260
# "rental_unit": "Gebäudeeinheit",
255261
# "time_period": "Mieter Abrechnungsperiode",
256-
# "verbrauch": "Warmwasser Verbrauch (Kubikmeter)",
262+
# "usage": "Warmwasser Verbrauch (Kubikmeter)",
257263
# },
258264
# },
259265
# },
@@ -301,7 +307,7 @@ def get_costs_from_config():
301307
"billing_group": "Wasserkosten",
302308
"vewa_category": NkCostVEWACategories.WATER_GENERAL,
303309
"base_cost_factor_key": "Wasserkosten:Grundkostenanteil",
304-
"exclude_zero_usage_units": True,
310+
"exclude_zero_usage_units": False,
305311
"measurement_data": {
306312
"building": {
307313
"class": NkMeasurementDataAnnual,
@@ -314,22 +320,11 @@ def get_costs_from_config():
314320
"headers": {
315321
"rental_unit": "Gebäudeeinheit",
316322
"time_period": "Mieter Abrechnungsperiode",
317-
"verbrauch": "Warmwasser Verbrauch (Kubikmeter)",
323+
"usage": "Warmwasser Verbrauch (Kubikmeter)",
318324
},
319325
},
320326
},
321327
},
322-
{
323-
"name": "Wasser_Abwasser_Grundkosten",
324-
"category": "waerme_wasser_grund",
325-
"amount_factor": 0.3,
326-
},
327-
{
328-
"name": "Wasser_Abwasser_Verbrauch",
329-
"category": "waerme_wasser_verbrauch",
330-
"amount_factor": 0.7,
331-
"object_weights": "messung_wasser",
332-
},
333328
{
334329
"name": "Fernwaerme_Fussboden_Grundkosten",
335330
"category": "waerme_wasser_grund",
@@ -364,6 +359,34 @@ def get_costs_from_config():
364359
"section_weights": "lueftung", #'default',
365360
"object_weights": "volume", #'area',
366361
},
362+
{
363+
"class": NkCostVEWA,
364+
"name": "Fernwaerme_Warmwasser",
365+
"billing_group": "Wärmekosten",
366+
"vewa_category": NkCostVEWACategories.HEAT_WATER,
367+
"base_cost_factor_key": "Warmwasser:Grundkostenanteil",
368+
"exclude_zero_usage_units": True,
369+
"measurement_data": {
370+
"building": {
371+
"class": NkMeasurementDataMonthlyCSVFile,
372+
"file_key": "Messdaten:Liegenschaft",
373+
"headers": {
374+
"month": "Monat",
375+
"costs": "Fernwaerme_Warmwasser",
376+
},
377+
},
378+
"rental_units": {
379+
"class": NkMeasurementDataEgon,
380+
"file_key": "Messdaten:Mieteinheiten",
381+
"file_prefix": "egon_Waerme",
382+
"headers": {
383+
"rental_unit": "Gebäudeeinheit",
384+
"time_period": "Mieter Abrechnungsperiode",
385+
"usage": "Warmwasser Verbrauch (Kubikmeter)",
386+
},
387+
},
388+
},
389+
},
367390
{
368391
"name": "Fernwaerme_Warmwasser_Grundkosten",
369392
"category": "waerme_wasser_grund",
@@ -398,7 +421,7 @@ def get_costs_from_config():
398421
"korrekturen_key": "Strom:Korrekturen",
399422
"measurement_data": {
400423
"building": {
401-
"class": NkMeasurementDataMonthly,
424+
"class": NkMeasurementDataMonthlyCSVFile,
402425
"file_key": "Messdaten:Liegenschaft",
403426
"headers": {
404427
"month": "Monat",

django/report/tests/test_nk_generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def test_load_costs(self):
4949
report.load_contracts()
5050
report.load_costs()
5151
self.assertEqual(report.get_warnings(), [])
52-
self.assertEqual(len(report.costs), 9)
52+
self.assertEqual(len(report.costs), 11)
5353
check_count = 0
5454
for cost in report.costs:
5555
check_count += 1

0 commit comments

Comments
 (0)