@@ -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 :
0 commit comments