Skip to content

Commit fd7b7a0

Browse files
Add context processing for VEWA, CostConfig struct.
1 parent 9bd5734 commit fd7b7a0

6 files changed

Lines changed: 471 additions & 97 deletions

File tree

django/report/nk/bill.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ def _get_rental_unit_context(
101101
)
102102
if cost_context:
103103
context["costs"].append(cost_context)
104+
aggregated_values = {}
104105
for cost in costs:
105-
context.update(cost.get_extra_context(ru, self.contract))
106+
# context.update(cost.get_extra_context(ru, self.contract))
107+
cost.update_context(ru, self.contract, context, aggregated_values)
106108
return context
107109

108110
def _create_rental_unit_files(self, context, ru):

django/report/nk/cost/base.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ def __init__(self, report_generator: "NkReportGenerator", cost_config: dict):
4545
self.generator = report_generator
4646
self.name = cost_config.get("name")
4747
self.billing_group = cost_config.get("billing_group", self.name)
48-
self.total_values = {}
49-
self.section_values = {}
50-
self.rental_unit_values = {}
48+
self.total_values: dict[NkCostValueType, NkCostValue] = {}
49+
self.section_values: dict[int, dict[NkCostValueType, NkCostValue]] = {}
50+
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")
5353

@@ -307,11 +307,14 @@ def _get_assigned_sum(
307307
def get_building_amount(self, value_type: NkCostValueType):
308308
return self.total_values[value_type].amount
309309

310-
def get_extra_context(self, ru: "NkRentalUnit", contract: "NkContract") -> dict:
310+
def _get_context(self, ru: "NkRentalUnit", contract: "NkContract") -> dict:
311311
"""Return extra context variables for ODT template rendering. Override in subclasses."""
312312
return {}
313313

314-
# def update_context(self, context, ru, contract):
314+
def update_context(
315+
self, ru: "NkRentalUnit", contract: "NkContract", context: dict, aggregated_values: dict
316+
) -> None:
317+
context.update(self._get_context(ru, contract))
315318

316319

317320
class NkMeasurementDataMixin:

django/report/nk/cost/vewa.py

Lines changed: 182 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def __init__(self, report_generator: "NkReportGenerator", cost_config: dict):
4343
config = self.generator.config
4444
self.base_cost_factor = float(config.get(cost_config.get("base_cost_factor_key"), 0.3))
4545
self.vewa_category = cost_config.get("vewa_category")
46+
self.exclude_zero_usage_units = cost_config.get("exclude_zero_usage_units", False)
4647
self._validate_config()
4748

4849
def load_building_totals(self):
@@ -97,6 +98,14 @@ def get_rental_unit_usage_weights(self, ru_id):
9798
ru_messung = self.measurements["rental_units"].get(ru.name, {})
9899
return ru_messung.get("verbrauch", self.generator.num_months * [0.0])
99100

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)
105+
106+
def _has_zero_usage(self, ru_id):
107+
return self.measurements["rental_units"].get(ru_id, {}).get("verbrauch", 0) == 0
108+
100109
def split_costs(self):
101110
# Base costs are handled by the super class
102111
super().split_costs()
@@ -113,91 +122,192 @@ def _calculate_usage_weights(self):
113122
NkCostValueType.USAGE_WEIGHT, "get_rental_unit_usage_weights"
114123
)
115124

116-
def get_extra_context(self, ru: "NkRentalUnit", contract: "NkContract") -> dict:
117-
"""Return Stromkosten detail variables for the ODT bill template."""
125+
def update_context(
126+
self, ru: "NkRentalUnit", contract: "NkContract", context: dict, aggregated_values: dict
127+
) -> None:
128+
context_key, context_prefix = self.get_context_key()
129+
if context_key not in context:
130+
context[context_key] = []
131+
cost_context = self._get_context(ru, contract)
132+
context[context_key].append(cost_context)
133+
self._update_aggregated_context(ru, contract, aggregated_values, context)
118134

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
135+
# Support for legacy templates: add context variables with fixed prefix for old templates,
136+
# which support only one cost per prefix.
137+
for key, value in cost_context.items():
138+
context[f"{context_prefix}_{key}"] = value
122139

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
140+
def get_context_key(self):
141+
"""Return the context key and prefix (for legecy templates) for the ODT bill template."""
142+
if self.vewa_category == NkCostVEWACategories.HEAT_WATER:
143+
context_key = "vewa_warmwasser"
144+
legacy_prefix = "ww"
145+
elif self.vewa_category == NkCostVEWACategories.HEAT_HEATING:
146+
if self.name == "Fernwaerme_Fussboden":
147+
legacy_prefix = "hf"
148+
elif self.name == "Fernwaerme_Radiatoren":
149+
legacy_prefix = "hr"
150+
elif self.name == "Fernwaerme_Lueftung":
151+
legacy_prefix = "hl"
152+
else:
153+
legacy_prefix = "h"
154+
context_key = "vewa_heizung"
155+
elif self.vewa_category == NkCostVEWACategories.WATER_GENERAL:
156+
legacy_prefix = "wa"
157+
context_key = "vewa_wasser"
158+
else:
159+
raise ValueError(
160+
_("Invalid VEWA category: {category}").format(category=self.vewa_category)
161+
)
162+
return context_key, legacy_prefix
163+
164+
def _get_context(self, ru: "NkRentalUnit", contract: "NkContract") -> dict:
165+
"""Return Stromkosten detail variables for the ODT bill template."""
128166

129167
def fmt(val):
130168
return nformat(val)
131169

132-
def fmt_kwh(val):
133-
return nformat(val, 0)
170+
def fmt_use(val):
171+
return nformat(val, 1)
134172

135-
def rate(chf, kwh):
136-
return nformat(chf / kwh if kwh else 0, 4)
173+
def rate(chf, use):
174+
return nformat(chf / use if use else 0, 2)
137175

138-
# Building totals (formatted)
176+
# Building totals
177+
bt = {
178+
"base": {
179+
"chf": self.total_values[NkCostValueType.COST].amount,
180+
"use": self.total_values[NkCostValueType.USAGE].amount,
181+
},
182+
"usage": {
183+
"chf": self.total_values[NkCostValueType.USAGE_COST].amount,
184+
"use": self.total_values[NkCostValueType.USAGE_USAGE].amount,
185+
},
186+
"common": {
187+
"chf": self.total_values[NkCostValueType.COMMON_COST].amount,
188+
"use": self.total_values[NkCostValueType.COMMON_USAGE].amount,
189+
},
190+
}
191+
# Assigned costs
192+
d = {
193+
"base": {
194+
"chf": self._get_assigned_amount(NkCostValueType.COST, contract, ru),
195+
"use": self._get_assigned_amount(NkCostValueType.USAGE, contract, ru),
196+
},
197+
"usage": {
198+
"chf": self._get_assigned_amount(NkCostValueType.USAGE_COST, contract, ru),
199+
"use": self._get_assigned_amount(NkCostValueType.USAGE_USAGE, contract, ru),
200+
},
201+
"common": {
202+
"chf": self._get_assigned_amount(NkCostValueType.COMMON_COST, contract, ru),
203+
"use": self._get_assigned_amount(NkCostValueType.COMMON_USAGE, contract, ru),
204+
},
205+
}
139206
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
207+
# Base costs
208+
"g_chft": fmt(bt["base"]["chf"]),
209+
"gt": fmt_use(bt["base"]["use"]),
210+
"g_eh": rate(bt["base"]["chf"], bt["base"]["use"]),
211+
"g": fmt_use(d["base"]["use"]),
212+
"g_chf": fmt(d["base"]["chf"]),
213+
# Usage costs
214+
"v_chft": fmt(bt["usage"]["chf"]),
215+
"vt": fmt_use(bt["usage"]["use"]),
216+
"v_eh": rate(bt["usage"]["chf"], bt["usage"]["use"]),
217+
"v": fmt_use(d["usage"]["use"]),
218+
"v_chf": fmt(d["usage"]["chf"]),
219+
# Base + Usage costs
220+
"_chft": fmt(bt["base"]["chf"] + bt["usage"]["chf"]),
221+
"t": fmt_use(bt["base"]["use"] + bt["usage"]["use"]),
222+
"_eh": rate(
223+
bt["base"]["chf"] + bt["usage"]["chf"], bt["base"]["use"] + bt["usage"]["use"]
186224
),
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),
225+
"": fmt_use(d["base"]["use"] + d["usage"]["use"]),
226+
"_chf": fmt(d["base"]["chf"] + d["usage"]["chf"]),
227+
# Common costs
228+
"a_chft": fmt(bt["common"]["chf"]),
229+
"at": fmt_use(bt["common"]["use"]),
230+
"a_eh": rate(bt["common"]["chf"], bt["common"]["use"]),
231+
"a": fmt_use(d["common"]["use"]),
232+
"a_chf": fmt(d["common"]["chf"]),
198233
}
199234
return ctx
200235

236+
def _update_aggregated_context(
237+
self, ru: "NkRentalUnit", contract: "NkContract", context: dict, aggregated_values: dict
238+
) -> None:
239+
if self.vewa_category == NkCostVEWACategories.HEAT_WATER:
240+
self._update_context_totals(
241+
["wwbt_chft", "sw_chft"],
242+
["wwbt_chf", "wwt_chf", "sw_chf"],
243+
["sw_chft", "wwt_chf", "sw_chf"],
244+
ru,
245+
contract,
246+
context,
247+
aggregated_values,
248+
)
249+
elif self.vewa_category == NkCostVEWACategories.HEAT_HEATING:
250+
self._update_context_totals(
251+
["ht_chft", "sw_chft"],
252+
["ht_chf", "sw_chf"],
253+
["sw_chft", "sw_chf"],
254+
ru,
255+
contract,
256+
context,
257+
aggregated_values,
258+
)
259+
elif self.vewa_category == NkCostVEWACategories.WATER_GENERAL:
260+
self._update_context_totals(
261+
["wat_chft", "swa_chft"],
262+
["wat_chf", "swa_chf"],
263+
["swa_chft", "swa_chf"],
264+
ru,
265+
contract,
266+
context,
267+
aggregated_values,
268+
)
269+
else:
270+
raise ValueError(
271+
_("Invalid VEWA category: {category}").format(category=self.vewa_category)
272+
)
273+
274+
def _update_context_totals(
275+
self,
276+
building_keys: list[str],
277+
unit_keys: list[str],
278+
include_common_keys: list[str],
279+
ru: "NkRentalUnit",
280+
contract: "NkContract",
281+
context: dict,
282+
aggregated_values: dict,
283+
) -> None:
284+
for key in building_keys + unit_keys:
285+
if key not in aggregated_values:
286+
aggregated_values[key] = 0
287+
building = (
288+
self.total_values[NkCostValueType.COST].amount
289+
+ self.total_values[NkCostValueType.USAGE_COST].amount
290+
)
291+
unit = self._get_assigned_amount(
292+
NkCostValueType.COST, contract, ru
293+
) + self._get_assigned_amount(NkCostValueType.USAGE_COST, contract, ru)
294+
common_building = self.total_values[NkCostValueType.COMMON_COST].amount
295+
common_unit = self._get_assigned_amount(NkCostValueType.COMMON_COST, contract, ru)
296+
# Building totals
297+
for key in building_keys:
298+
aggregated_values[key] += building
299+
if key in include_common_keys:
300+
# Building totals including the common usage
301+
aggregated_values[key] += common_building
302+
# Unit totals
303+
for key in unit_keys:
304+
aggregated_values[key] += unit
305+
if key in include_common_keys:
306+
# Unit totals including the common usage
307+
aggregated_values[key] += common_unit
308+
for key in building_keys + unit_keys:
309+
context[key] = nformat(aggregated_values.get(key, 0))
310+
201311
def get_export_extra_info(
202312
self, include_percent: bool = False, formatter: Callable = lambda x: x
203313
) -> list:

django/report/nk/cost/zev.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def split_costs(self):
216216
self._split_common_costs()
217217
self._aggregate_monthly_amounts()
218218

219-
def get_extra_context(self, ru: "NkRentalUnit", contract: "NkContract") -> dict:
219+
def _get_context(self, ru: "NkRentalUnit", contract: "NkContract") -> dict:
220220
"""Return Stromkosten detail variables for the ODT bill template."""
221221

222222
ru_data = self._strom_data.get(ru.id, self._zero_strom_data(self.generator.num_months))
@@ -238,7 +238,6 @@ def fmt_kwh(val):
238238
def rate(chf, kwh):
239239
return nformat(chf / kwh if kwh else 0, 4)
240240

241-
# Building totals (formatted)
242241
ctx = {
243242
# Eigenverbrauch Solar direkt (from roof)
244243
"ssd_chft": fmt(bt["ssd"]["chf"]),

0 commit comments

Comments
 (0)