Skip to content

Commit 0773ca7

Browse files
Add common cost split and corrections for ZEV.
1 parent df812fc commit 0773ca7

14 files changed

Lines changed: 594 additions & 223 deletions

File tree

django/geno/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ def nformat(number, precision=2, round_to=False, thousands_separator="'"):
221221
return format(number, ".%df" % precision)
222222

223223

224+
def unformat(number: str | float, thousands_separator="'"):
225+
if isinstance(number, str):
226+
number = number.replace(thousands_separator, "").replace("%", "")
227+
return float(number)
228+
229+
224230
def sanitize_filename(filename):
225231
normalized_filename = filename.replace("+", "-").replace("/", "-")
226232
return re.sub(r"[^\w\-_\.]+", "", normalized_filename)

django/geno/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1609,7 +1609,7 @@ def get_context_data(self, **kwargs):
16091609

16101610

16111611
class ShareInterestView(CohivaAdminViewMixin, TemplateView):
1612-
title = _("Zinsabrechung")
1612+
title = _("Zinsabrechnung")
16131613
permission_required = (
16141614
"geno.canview_share",
16151615
"geno.canview_billing",

django/report/nk/bill.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,12 @@ def _get_billing_groups(costs: list[NkCost]):
145145
return billing_groups
146146

147147
def _get_billing_group_context(
148-
self, name, group, rental_unit, total_building_cost, total_ru_cost
148+
self, name, group: list[NkCost], rental_unit, total_building_cost, total_ru_cost
149149
):
150150
object_cost = 0
151151
building_cost = 0
152152
for cost in group:
153-
object_cost += cost.get_assigned_amount(
154-
NkCostValueType.COST, self.contract, rental_unit
155-
)
153+
object_cost += cost.get_assigned_cost(self.contract, rental_unit)
156154
building_cost += cost.get_building_amount(NkCostValueType.COST)
157155
if object_cost == 0 and building_cost == 0:
158156
return None

django/report/nk/contract.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from cohiva.utils.countries import normalize_country_code
77
from finance.accounting import Account, AccountKey
88
from geno.models import Address, Contract, Invoice, InvoiceCategory
9-
from report.nk.cost import NkCost, NkCostValueType
9+
from report.nk.cost import NkCost
1010
from report.nk.rental_unit import NkRentalUnit
1111

1212

@@ -178,7 +178,7 @@ def akonto_nominal(self):
178178
def get_total_costs(self, costs: list[NkCost], rental_unit: NkRentalUnit | None = None):
179179
ret = 0
180180
for cost in costs:
181-
ret += cost.get_assigned_amount(NkCostValueType.COST, self, rental_unit)
181+
ret += cost.get_assigned_cost(self, rental_unit)
182182
return ret
183183

184184
def get_paid_akonto(self, ru: NkRentalUnit) -> float:

django/report/nk/cost/base.py

Lines changed: 123 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ class NkCostValueType(Enum):
1212
COST = 1 # The costs that are billed
1313
USAGE = 2 # The usage that is billed (consumed energy, rental unit area, etc.)
1414
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
1520

1621

1722
@dataclass
@@ -106,25 +111,38 @@ def _normalize_monthly_amounts_for_dict(self, container: dict, value_required=Fa
106111
def split_costs(self):
107112
self._calculate_weights()
108113
for kind in self.total_values:
109-
if kind == NkCostValueType.WEIGHT:
110-
continue
111-
amount_per_weight = (
112-
(self.total_values[kind].amount / self.total_values[NkCostValueType.WEIGHT].amount)
113-
if self.total_values[NkCostValueType.WEIGHT].amount
114-
else 0
115-
)
116-
self._calculate_amounts(self.total_values, kind, amount_per_weight)
117-
for ru in self.generator.rental_units:
118-
self._calculate_amounts(self.rental_unit_values[ru.id], kind, amount_per_weight)
119-
for section in self.generator.sections:
120-
self._calculate_amounts(self.section_values[section.id], kind, amount_per_weight)
114+
if kind in (NkCostValueType.COST, NkCostValueType.USAGE):
115+
self._split_cost(kind, NkCostValueType.WEIGHT)
121116

122117
def update(self):
123118
pass
124119

125-
def _calculate_amounts(self, values, kind: NkCostValueType, amount_per_weight):
120+
def _split_cost(self, cost_type, weight_type):
121+
amount_per_weight = self.generator.num_months * [0]
122+
for month in range(self.generator.num_months):
123+
amount_per_weight[month] = (
124+
(
125+
self.total_values[cost_type].monthly_amounts[month]
126+
/ self.total_values[weight_type].monthly_amounts[month]
127+
)
128+
if self.total_values[weight_type].monthly_amounts[month]
129+
else 0
130+
)
131+
self._calculate_amounts(self.total_values, cost_type, weight_type, amount_per_weight)
132+
for ru in self.generator.rental_units:
133+
self._calculate_amounts(
134+
self.rental_unit_values[ru.id], cost_type, weight_type, amount_per_weight
135+
)
136+
for section in self.generator.sections:
137+
self._calculate_amounts(
138+
self.section_values[section.id], cost_type, weight_type, amount_per_weight
139+
)
140+
141+
def _calculate_amounts(
142+
self, values, kind: NkCostValueType, weight_type: NkCostValueType, amount_per_weight
143+
):
126144
for month in range(self.generator.num_months):
127-
amount = amount_per_weight * values[NkCostValueType.WEIGHT].monthly_amounts[month]
145+
amount = amount_per_weight[month] * values[weight_type].monthly_amounts[month]
128146
if (
129147
values[kind].monthly_amounts[month]
130148
and abs(values[kind].monthly_amounts[month] - amount) > 0.01
@@ -146,14 +164,22 @@ def _calculate_amounts(self, values, kind: NkCostValueType, amount_per_weight):
146164
values[kind].amount = total_amount
147165

148166
def _calculate_weights(self):
149-
self.add_value_type(NkCostValueType.WEIGHT, "Gewichtung", "")
167+
self._calculate_weights_for_type(NkCostValueType.WEIGHT, "get_rental_unit_weights")
168+
169+
def _calculate_weights_for_type(
170+
self, value_type: NkCostValueType, rental_unit_weights_function_name: str
171+
):
172+
self.add_value_type(value_type, "Gewichtung", "")
150173
monthly_weights = self.get_monthly_weights()
151174
section_weights = self.get_section_weights()
152-
total = self.total_values[NkCostValueType.WEIGHT]
175+
total = self.total_values[value_type]
176+
rental_unit_weights_function = getattr(self, rental_unit_weights_function_name)
177+
if not callable(rental_unit_weights_function):
178+
raise ValueError(f"Invalid function name: {rental_unit_weights_function_name}")
153179
for ru in self.generator.rental_units:
154-
ru_weights = self.get_rental_unit_weights(ru.id)
155-
values = self.rental_unit_values[ru.id][NkCostValueType.WEIGHT]
156-
section = self.section_values[ru.section.id][NkCostValueType.WEIGHT]
180+
ru_weights = rental_unit_weights_function(ru.id)
181+
values = self.rental_unit_values[ru.id][value_type]
182+
section = self.section_values[ru.section.id][value_type]
157183
for month in range(self.generator.num_months):
158184
weight = (
159185
monthly_weights[month] * section_weights[ru.section.id] * ru_weights[month]
@@ -163,8 +189,8 @@ def _calculate_weights(self):
163189
total.monthly_amounts[month] += weight
164190
values.amount = sum(values.monthly_amounts)
165191
for section in self.generator.sections:
166-
self.section_values[section.id][NkCostValueType.WEIGHT].amount = sum(
167-
self.section_values[section.id][NkCostValueType.WEIGHT].monthly_amounts
192+
self.section_values[section.id][value_type].amount = sum(
193+
self.section_values[section.id][value_type].monthly_amounts
168194
)
169195
total.amount = sum(total.monthly_amounts)
170196

@@ -217,6 +243,9 @@ def get_export_weight_row(self, include_percent=False):
217243
row = self._get_export_row(NkCostValueType.WEIGHT, include_percent)
218244
return row
219245

246+
def get_export_extra_info(self, include_percent=False, formatter=None):
247+
return None
248+
220249
def _get_export_row(self, kind, include_percent):
221250
## TODO: implement include_percent (if still needed)
222251
row = [self.name]
@@ -230,7 +259,7 @@ def _get_export_row(self, kind, include_percent):
230259
row.append(self.rental_unit_values[ru.id][kind].amount)
231260
return row
232261

233-
def get_assigned_amount(
262+
def _get_assigned_amount(
234263
self,
235264
value_type: NkCostValueType,
236265
contract: "NkContract",
@@ -239,12 +268,37 @@ def get_assigned_amount(
239268
ret = 0
240269
rental_units = [rental_unit] if rental_unit else contract.rental_units
241270
for ru in rental_units:
242-
for idx, amount in enumerate(
243-
self.rental_unit_values[ru.id][value_type].monthly_amounts
244-
):
245-
assigned_contract = ru.get_assigned_contract_for_month(idx)
246-
if assigned_contract == contract:
247-
ret += amount
271+
ret += self._get_assigned_sum(
272+
self.rental_unit_values[ru.id][value_type].monthly_amounts, contract, ru
273+
)
274+
return ret
275+
276+
def get_assigned_cost(self, contract: "NkContract", rental_unit: "NkRentalUnit | None" = None):
277+
return self._get_assigned_amount(NkCostValueType.COST, contract, rental_unit)
278+
279+
@classmethod
280+
def get_assigned_amounts(
281+
cls,
282+
data: dict[str, list[float]],
283+
contract: "NkContract",
284+
rental_unit: "NkRentalUnit",
285+
):
286+
ret = {}
287+
for kind, monthly_values in data.items():
288+
ret[kind] = cls._get_assigned_sum(monthly_values, contract, rental_unit)
289+
return ret
290+
291+
@staticmethod
292+
def _get_assigned_sum(
293+
monthly_values: list[float],
294+
contract: "NkContract",
295+
rental_unit: "NkRentalUnit",
296+
):
297+
ret = 0
298+
for idx, amount in enumerate(monthly_values):
299+
assigned_contract = rental_unit.get_assigned_contract_for_month(idx)
300+
if assigned_contract == contract:
301+
ret += amount
248302
return ret
249303

250304
def get_building_amount(self, value_type: NkCostValueType):
@@ -276,3 +330,44 @@ def load_input_data(self):
276330
print(warning)
277331
self.generator.add_warning(warning[0], warning[1])
278332
super().load_input_data()
333+
334+
335+
class NkCommonCostMixin:
336+
"""Mixin for NkCosts that have a common usage part (e.g., Allgemeinstrom), which is distributed among all rental units."""
337+
338+
def __init__(self, report_generator: "NkReportGenerator", cost_config: dict):
339+
super().__init__(report_generator, cost_config)
340+
self.add_value_type(NkCostValueType.COMMON_COST, "Allgemeinkosten", "CHF")
341+
342+
def get_assigned_cost(self, contract: "NkContract", rental_unit: "NkRentalUnit | None" = None):
343+
ret = super().get_assigned_cost(contract, rental_unit)
344+
return ret + self._get_assigned_amount(NkCostValueType.COMMON_COST, contract, rental_unit)
345+
346+
def get_rental_unit_common_weights(self, ru_id):
347+
"""Default is the rental unit area (per period)."""
348+
ru = self.generator.get_rental_unit_by_id(ru_id)
349+
if ru.is_virtual:
350+
return self.generator.num_months * [0.0]
351+
else:
352+
return self.generator.num_months * [ru.area / self.generator.num_months]
353+
354+
def set_common_costs(self, cost: float | list[float], usage: float | list[float] | None):
355+
if isinstance(cost, list):
356+
self.total_values[NkCostValueType.COMMON_COST].monthly_amounts = cost
357+
else:
358+
self.total_values[NkCostValueType.COMMON_COST].amount = cost
359+
if usage:
360+
if isinstance(usage, list):
361+
self.total_values[NkCostValueType.COMMON_USAGE].monthly_amounts = usage
362+
else:
363+
self.total_values[NkCostValueType.COMMON_USAGE].amount = usage
364+
365+
def _split_common_costs(self):
366+
self._calculate_common_weights()
367+
for kind in (NkCostValueType.COMMON_COST, NkCostValueType.COMMON_USAGE):
368+
self._split_cost(kind, NkCostValueType.COMMON_WEIGHT)
369+
370+
def _calculate_common_weights(self):
371+
self._calculate_weights_for_type(
372+
NkCostValueType.COMMON_WEIGHT, "get_rental_unit_common_weights"
373+
)

0 commit comments

Comments
 (0)