|
| 1 | +from typing import TYPE_CHECKING |
| 2 | + |
| 3 | +from geno.utils import nformat |
| 4 | + |
| 5 | +from .base import NkCost, NkCostValueType |
| 6 | + |
| 7 | +if TYPE_CHECKING: |
| 8 | + from report.nk.contract import NkContract |
| 9 | + from report.nk.generator import NkReportGenerator |
| 10 | + from report.nk.rental_unit import NkRentalUnit |
| 11 | + |
| 12 | + |
| 13 | +class NkCostZEVStromallmend(NkCost): |
| 14 | + """ZEV electricity costs calculated individually per rental unit. |
| 15 | +
|
| 16 | + Mirrors the logic from the old report_nk.stromrechnung(): |
| 17 | + - Reads tariffs from report config (Strom:Tarif:*) |
| 18 | + - Reads per-unit measurement data from report.object_messung[ru.name]: |
| 19 | + strom_solar, strom_ew_hoch, strom_ew_nieder, chf_netz_hoch, chf_netz_nieder |
| 20 | + - Reads building-level data from report.data_amount: |
| 21 | + Strom_kwh_egon, Strom_kwh_ruecklieferung |
| 22 | + - Computes per-unit costs and injects detailed context variables for the |
| 23 | + "Stromkosten" section in the ODT bill template. |
| 24 | +
|
| 25 | + ODT template variable naming convention: |
| 26 | + ssd_* Eigenverbrauch Solar direkt (from roof) |
| 27 | + sss_* Eigenverbrauch Solar via Speicher/Stromallmend |
| 28 | + snh_* Netzstrombezug Hochtarif |
| 29 | + snt_* Netzstrombezug Niedertarif |
| 30 | + shk_* Herkunftsnachweise (HKN) for purchased solar |
| 31 | + sk_* Korrektur (manual correction) |
| 32 | + st_* Strom subtotal (before Allgemeinstrom and fees) |
| 33 | + sa_* Anteil Allgemeinstrom (communal electricity share) |
| 34 | + snk_* Stromnebenkosten / Messung |
| 35 | + stot_* Grand total |
| 36 | +
|
| 37 | + Suffixes: |
| 38 | + _chft Building total CHF (formatted string) |
| 39 | + _t Building total kWh (number) |
| 40 | + _eh CHF/kWh rate |
| 41 | + (none) Rental unit kWh |
| 42 | + _chf Rental unit CHF (formatted string) |
| 43 | + """ |
| 44 | + |
| 45 | + cost_type_id = "zev_stromallmend" |
| 46 | + |
| 47 | + def __init__(self, report: "NkReportGenerator", cost_config: dict): |
| 48 | + super().__init__(report, cost_config) |
| 49 | + self.add_value_type(NkCostValueType.USAGE, "Verbrauch", "kWh") |
| 50 | + # Per-unit intermediate data: ru_id -> dict |
| 51 | + self._strom_data: dict[int, dict] = {} |
| 52 | + # Building-level totals (populated in load_input_data) |
| 53 | + self._building_totals: dict = {} |
| 54 | + |
| 55 | + def load_input_data(self): |
| 56 | + super().load_input_data() |
| 57 | + |
| 58 | + config = self.report.config |
| 59 | + tarif_eigenstrom = config.get("Strom:Tarif:Eigenstrom", 0) |
| 60 | + tarif_einspeiseverguetung = config.get("Strom:Tarif:Einspeisevergütung", 12 * [0]) |
| 61 | + tarif_hkn = config.get("Strom:Tarif:HKN", 0) |
| 62 | + tarif_korrektur = config.get("Strom:Tarif:Korrekturen", {"mittel": 0, "nacht": 0}) |
| 63 | + |
| 64 | + num_months = self.report.num_months |
| 65 | + |
| 66 | + # Building-level: einspeisefaktor per month |
| 67 | + kwh_egon = self.report.data_amount.get("Strom_kwh_egon", num_months * [0]) |
| 68 | + kwh_ruecklieferung = self.report.data_amount.get( |
| 69 | + "Strom_kwh_ruecklieferung", num_months * [0] |
| 70 | + ) |
| 71 | + einspeisefaktor = [] |
| 72 | + for m in range(num_months): |
| 73 | + if kwh_egon[m]: |
| 74 | + einspeisefaktor.append(kwh_ruecklieferung[m] / kwh_egon[m]) |
| 75 | + else: |
| 76 | + einspeisefaktor.append(0) |
| 77 | + |
| 78 | + # Building-level totals accumulators |
| 79 | + totals = { |
| 80 | + "ssd": {"kwh": num_months * [0.0], "chf": num_months * [0.0]}, # solar direkt |
| 81 | + "sss": {"kwh": num_months * [0.0], "chf": num_months * [0.0]}, # solar speicher |
| 82 | + "snh": {"kwh": num_months * [0.0], "chf": num_months * [0.0]}, # netz hoch |
| 83 | + "snt": {"kwh": num_months * [0.0], "chf": num_months * [0.0]}, # netz nieder |
| 84 | + "shk": {"kwh": num_months * [0.0], "chf": num_months * [0.0]}, # HKN einkauf |
| 85 | + "sk": {"kwh": num_months * [0.0], "chf": num_months * [0.0]}, # korrektur |
| 86 | + "total": {"kwh": num_months * [0.0], "chf": num_months * [0.0]}, |
| 87 | + } |
| 88 | + |
| 89 | + for ru in self.report.rental_units: |
| 90 | + if ru.is_virtual: |
| 91 | + self._strom_data[ru.id] = self._zero_strom_data(num_months) |
| 92 | + continue |
| 93 | + |
| 94 | + ru_messung = self.report.object_messung.get(ru.name, {}) |
| 95 | + if "strom_solar" not in ru_messung: |
| 96 | + self._strom_data[ru.id] = self._zero_strom_data(num_months) |
| 97 | + continue |
| 98 | + |
| 99 | + messung_solar = ru_messung.get("strom_solar", num_months * [0.0]) |
| 100 | + messung_ew_hoch = ru_messung.get("strom_ew_hoch", num_months * [0.0]) |
| 101 | + messung_ew_nieder = ru_messung.get("strom_ew_nieder", num_months * [0.0]) |
| 102 | + messung_chf_hoch = ru_messung.get("chf_netz_hoch", num_months * [0.0]) |
| 103 | + messung_chf_nieder = ru_messung.get("chf_netz_nieder", num_months * [0.0]) |
| 104 | + |
| 105 | + # Korrekturen from config |
| 106 | + kwh_korrektur = num_months * [0.0] |
| 107 | + chf_korrektur = num_months * [0.0] |
| 108 | + korrektur_config = config.get("Strom:Korrekturen", {}) |
| 109 | + if ru.name in korrektur_config: |
| 110 | + for korr in korrektur_config[ru.name]: |
| 111 | + tarif = tarif_korrektur.get(korr.get("tarif", "mittel"), 0) |
| 112 | + for m in range(num_months): |
| 113 | + kwh_korrektur[m] += korr["kwh"][m] |
| 114 | + chf_korrektur[m] += korr["kwh"][m] * tarif |
| 115 | + |
| 116 | + d = {} |
| 117 | + d["kwh_solar"] = list(messung_solar) |
| 118 | + d["kwh_solar_speicher"] = [] |
| 119 | + d["kwh_solar_einkauf"] = [] |
| 120 | + d["kwh_netzstrom"] = [] |
| 121 | + d["kwh_total"] = [] |
| 122 | + d["chf_solar_eigen"] = [] |
| 123 | + d["chf_solar_speicher"] = [] |
| 124 | + d["chf_solar_hkn"] = [] |
| 125 | + d["chf_netz_hoch"] = list(messung_chf_hoch) |
| 126 | + d["chf_netz_nieder"] = list(messung_chf_nieder) |
| 127 | + d["kwh_korrektur"] = kwh_korrektur |
| 128 | + d["chf_korrektur"] = chf_korrektur |
| 129 | + d["chf_total"] = [] |
| 130 | + |
| 131 | + for m in range(num_months): |
| 132 | + kwh_netz = messung_ew_hoch[m] + messung_ew_nieder[m] |
| 133 | + kwh_speicher = einspeisefaktor[m] * kwh_netz |
| 134 | + kwh_einkauf = kwh_netz - kwh_speicher |
| 135 | + kwh_tot = ( |
| 136 | + messung_solar[m] |
| 137 | + + kwh_speicher |
| 138 | + + kwh_einkauf |
| 139 | + + kwh_korrektur[m] |
| 140 | + ) |
| 141 | + |
| 142 | + chf_eigen = messung_solar[m] * tarif_eigenstrom |
| 143 | + tarif_einsp = ( |
| 144 | + tarif_einspeiseverguetung[m] |
| 145 | + if m < len(tarif_einspeiseverguetung) |
| 146 | + else 0 |
| 147 | + ) |
| 148 | + chf_speicher = kwh_speicher * (tarif_eigenstrom - tarif_einsp) |
| 149 | + chf_hkn = kwh_einkauf * tarif_hkn |
| 150 | + chf_tot = ( |
| 151 | + chf_eigen |
| 152 | + + chf_speicher |
| 153 | + + chf_hkn |
| 154 | + + messung_chf_hoch[m] |
| 155 | + + messung_chf_nieder[m] |
| 156 | + + chf_korrektur[m] |
| 157 | + ) |
| 158 | + |
| 159 | + d["kwh_solar_speicher"].append(kwh_speicher) |
| 160 | + d["kwh_solar_einkauf"].append(kwh_einkauf) |
| 161 | + d["kwh_netzstrom"].append(kwh_netz) |
| 162 | + d["kwh_total"].append(kwh_tot) |
| 163 | + d["chf_solar_eigen"].append(chf_eigen) |
| 164 | + d["chf_solar_speicher"].append(chf_speicher) |
| 165 | + d["chf_solar_hkn"].append(chf_hkn) |
| 166 | + d["chf_total"].append(chf_tot) |
| 167 | + |
| 168 | + totals["ssd"]["kwh"][m] += messung_solar[m] |
| 169 | + totals["ssd"]["chf"][m] += chf_eigen |
| 170 | + totals["sss"]["kwh"][m] += kwh_speicher |
| 171 | + totals["sss"]["chf"][m] += chf_speicher |
| 172 | + totals["snh"]["kwh"][m] += messung_ew_hoch[m] |
| 173 | + totals["snh"]["chf"][m] += messung_chf_hoch[m] |
| 174 | + totals["snt"]["kwh"][m] += messung_ew_nieder[m] |
| 175 | + totals["snt"]["chf"][m] += messung_chf_nieder[m] |
| 176 | + totals["shk"]["kwh"][m] += kwh_einkauf |
| 177 | + totals["shk"]["chf"][m] += chf_hkn |
| 178 | + totals["sk"]["kwh"][m] += kwh_korrektur[m] |
| 179 | + totals["sk"]["chf"][m] += chf_korrektur[m] |
| 180 | + totals["total"]["kwh"][m] += kwh_tot |
| 181 | + totals["total"]["chf"][m] += chf_tot |
| 182 | + |
| 183 | + self._strom_data[ru.id] = d |
| 184 | + |
| 185 | + # Store building totals as simple sums |
| 186 | + self._building_totals = { |
| 187 | + k: { |
| 188 | + "kwh": sum(v["kwh"]), |
| 189 | + "chf": sum(v["chf"]), |
| 190 | + } |
| 191 | + for k, v in totals.items() |
| 192 | + } |
| 193 | + self._building_totals_monthly = totals |
| 194 | + |
| 195 | + # Set COST and USAGE totals for the base cost aggregation |
| 196 | + self.total_values[NkCostValueType.COST].amount = self._building_totals["total"]["chf"] |
| 197 | + self.total_values[NkCostValueType.USAGE].amount = self._building_totals["total"]["kwh"] |
| 198 | + for ru in self.report.rental_units: |
| 199 | + d = self._strom_data[ru.id] |
| 200 | + self.rental_unit_values[ru.id][NkCostValueType.COST].amount = sum(d["chf_total"]) |
| 201 | + self.rental_unit_values[ru.id][NkCostValueType.USAGE].amount = sum(d["kwh_total"]) |
| 202 | + monthly_amounts = d["chf_total"] |
| 203 | + self.rental_unit_values[ru.id][NkCostValueType.COST].monthly_amounts = monthly_amounts |
| 204 | + |
| 205 | + def split_costs(self): |
| 206 | + """Aggregate pre-calculated per-rental-unit costs up to sections and total.""" |
| 207 | + for ru in self.report.rental_units: |
| 208 | + for month in range(self.report.num_months): |
| 209 | + amount = self.rental_unit_values[ru.id][NkCostValueType.COST].monthly_amounts[month] |
| 210 | + self.section_values[ru.section.id][NkCostValueType.COST].monthly_amounts[month] += amount |
| 211 | + self.total_values[NkCostValueType.COST].monthly_amounts[month] += amount |
| 212 | + |
| 213 | + for section in self.report.sections: |
| 214 | + self.section_values[section.id][NkCostValueType.COST].amount = sum( |
| 215 | + self.section_values[section.id][NkCostValueType.COST].monthly_amounts |
| 216 | + ) |
| 217 | + self.total_values[NkCostValueType.COST].amount = sum( |
| 218 | + self.total_values[NkCostValueType.COST].monthly_amounts |
| 219 | + ) |
| 220 | + |
| 221 | + def get_extra_context(self, ru: "NkRentalUnit", contract: "NkContract") -> dict: |
| 222 | + """Return Stromkosten detail variables for the ODT bill template.""" |
| 223 | + d = self._strom_data.get(ru.id, self._zero_strom_data(self.report.num_months)) |
| 224 | + bt = self._building_totals |
| 225 | + |
| 226 | + def fmt(val): |
| 227 | + return nformat(val) |
| 228 | + |
| 229 | + def rate(chf, kwh): |
| 230 | + return nformat(chf / kwh if kwh else 0, 4) |
| 231 | + |
| 232 | + # Building totals (formatted) |
| 233 | + ctx = { |
| 234 | + # Eigenverbrauch Solar direkt (from roof) |
| 235 | + "ssd_chft": fmt(bt["ssd"]["chf"]), |
| 236 | + "ssdt": bt["ssd"]["kwh"], |
| 237 | + "ssd_eh": rate(bt["ssd"]["chf"], bt["ssd"]["kwh"]), |
| 238 | + "ssd": sum(d["kwh_solar"]), |
| 239 | + "ssd_chf": fmt(sum(d["chf_solar_eigen"])), |
| 240 | + # Eigenverbrauch Solar via Speicher/Stromallmend |
| 241 | + "sss_chft": fmt(bt["sss"]["chf"]), |
| 242 | + "ssst": bt["sss"]["kwh"], |
| 243 | + "sss_eh": rate(bt["sss"]["chf"], bt["sss"]["kwh"]), |
| 244 | + "sss": sum(d["kwh_solar_speicher"]), |
| 245 | + "sss_chf": fmt(sum(d["chf_solar_speicher"])), |
| 246 | + # Netzstrombezug Hochtarif |
| 247 | + "snh_chft": fmt(bt["snh"]["chf"]), |
| 248 | + "snht": bt["snh"]["kwh"], |
| 249 | + "snh_eh": rate(bt["snh"]["chf"], bt["snh"]["kwh"]), |
| 250 | + "snh": sum(d["kwh_netzstrom"]), |
| 251 | + "snh_chf": fmt(sum(d["chf_netz_hoch"])), |
| 252 | + # Netzstrombezug Niedertarif |
| 253 | + "snt_chft": fmt(bt["snt"]["chf"]), |
| 254 | + "sntt": bt["snt"]["kwh"], |
| 255 | + "snt_eh": rate(bt["snt"]["chf"], bt["snt"]["kwh"]), |
| 256 | + "snt": sum(d["kwh_netzstrom"]), # combined with Hoch for total netz |
| 257 | + "snt_chf": fmt(sum(d["chf_netz_nieder"])), |
| 258 | + # Herkunftsnachweise (HKN) |
| 259 | + "shk_chft": fmt(bt["shk"]["chf"]), |
| 260 | + "shkt": bt["shk"]["kwh"], |
| 261 | + "shk_eh": rate(bt["shk"]["chf"], bt["shk"]["kwh"]), |
| 262 | + "shk": sum(d["kwh_solar_einkauf"]), |
| 263 | + "shk_chf": fmt(sum(d["chf_solar_hkn"])), |
| 264 | + # Korrektur |
| 265 | + "sk_chft": fmt(bt["sk"]["chf"]), |
| 266 | + "skt": bt["sk"]["kwh"], |
| 267 | + "sk_eh": rate(bt["sk"]["chf"], bt["sk"]["kwh"]), |
| 268 | + "sk": sum(d["kwh_korrektur"]), |
| 269 | + "sk_chf": fmt(sum(d["chf_korrektur"])), |
| 270 | + # Strom subtotal (sum of above, no separate Allgemeinstrom/fees in this class) |
| 271 | + "st_chft": fmt(bt["total"]["chf"]), |
| 272 | + "stt": bt["total"]["kwh"], |
| 273 | + "st": sum(d["kwh_total"]), |
| 274 | + "st_chf": fmt(sum(d["chf_total"])), |
| 275 | + # Anteil Allgemeinstrom (not computed by this class – leave empty) |
| 276 | + "sa_chft": "", |
| 277 | + "sat": "", |
| 278 | + "sa_eh": "", |
| 279 | + "sa": "", |
| 280 | + "sa_chf": "", |
| 281 | + # Stromnebenkosten/Messung (not computed by this class – leave empty) |
| 282 | + "snk_chft": "", |
| 283 | + "snkt": "", |
| 284 | + "snk_eh": "", |
| 285 | + "snk": "", |
| 286 | + "snk_chf": "", |
| 287 | + # Grand total |
| 288 | + "stot_chft": fmt(bt["total"]["chf"]), |
| 289 | + "stot_chf": fmt(sum(d["chf_total"])), |
| 290 | + } |
| 291 | + return ctx |
| 292 | + |
| 293 | + @staticmethod |
| 294 | + def _zero_strom_data(num_months: int) -> dict: |
| 295 | + zeros = num_months * [0.0] |
| 296 | + return { |
| 297 | + "kwh_solar": list(zeros), |
| 298 | + "kwh_solar_speicher": list(zeros), |
| 299 | + "kwh_solar_einkauf": list(zeros), |
| 300 | + "kwh_netzstrom": list(zeros), |
| 301 | + "kwh_total": list(zeros), |
| 302 | + "chf_solar_eigen": list(zeros), |
| 303 | + "chf_solar_speicher": list(zeros), |
| 304 | + "chf_solar_hkn": list(zeros), |
| 305 | + "chf_netz_hoch": list(zeros), |
| 306 | + "chf_netz_nieder": list(zeros), |
| 307 | + "kwh_korrektur": list(zeros), |
| 308 | + "chf_korrektur": list(zeros), |
| 309 | + "chf_total": list(zeros), |
| 310 | + } |
0 commit comments