Coverage for backend/django/Economics/formulas/builders/capital.py: 76%
238 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +0000
1from __future__ import annotations
3from dataclasses import dataclass
4from decimal import Decimal, ROUND_HALF_UP
5from typing import Mapping
7import sympy
9from Economics.shared.choices import CapitalLineBasis, CostBasis, CostCurveEvaluationKind
11from Economics.reference_data.models import CostIndexValue
13from Economics.settings_profiles.models import EconomicsSettingsProfile
14from Economics.costing.capital.capital_line_sources import GENERATED_CAPITAL_LINE_SOURCE
15from Economics.costing.capital.electrical_upgrade import derive_peak_demand_basis
16from Economics.costing.capital.lang_factors import resolve_lang_factor
17from Economics.settings_profiles.services.settings_profiles import get_settings_profile
19from Economics.formulas.engine.core import EconomicsFormula, FormulaError, FormulaInput, FormulaStep, decimal_to_sympy
20from Economics.formulas.engine.parsing import parse_cost_expression
23@dataclass(frozen=True)
24class CapitalIndexAdjustment:
25 factor: Decimal
26 detail: str
29@dataclass(frozen=True)
30class GeneratedCapitalLineFormula:
31 formula: EconomicsFormula
32 index_adjustment: CapitalIndexAdjustment
33 lang_factor: Decimal | None
34 lang_factor_source: str
35 applies_lang_factor: bool
36 contingency_percent: Decimal
37 contingency_factor: Decimal
40@dataclass(frozen=True)
41class BoundCapitalFormula:
42 formula: EconomicsFormula
43 bindings: Mapping[str, Decimal]
45 def evaluate(self) -> Decimal | None:
46 return self.formula.evaluate(self.bindings)
48 def render_property_formula(self, render_bindings: Mapping[str, str] | None = None) -> str:
49 rendered_bindings = {key: _decimal_literal(value) for key, value in self.bindings.items()}
50 rendered_bindings.update(render_bindings or {})
51 return self.formula.render_property_formula(rendered_bindings)
54CUSTOM_CAPEX_PERCENTAGE_BASIS = "custom_capex_percentage_basis"
55GENERATED_UNIT_CAPEX_SUBTOTAL = "generated_unit_capex_subtotal"
56CUSTOM_CAPITAL_TOTAL = "custom_capital_total"
57ELECTRICAL_UPGRADE_CAPEX = "electrical_upgrade_capex"
58PEAK_DEMAND_BASIS_KW = "peak_demand_kw"
59ELECTRICAL_UPGRADE_RATE = "electrical_upgrade_rate"
60CAPITAL_RESULT_QUANTUM = Decimal("0.0001")
63def build_cost_curve_formula(curve) -> EconomicsFormula:
64 """Build the canonical formula for an expression cost curve."""
65 if curve.evaluation_kind != CostCurveEvaluationKind.EXPRESSION: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true
66 raise FormulaError(
67 "invalid_cost_curve_evaluation_kind",
68 "Only expression curves have a top-level cost formula.",
69 context={"curve_key": curve.curve_key, "evaluation_kind": curve.evaluation_kind},
70 )
71 expression_text = (curve.expression_text or "").strip()
72 if not expression_text: 72 ↛ 73line 72 didn't jump to line 73 because the condition on line 72 was never true
73 raise FormulaError(
74 "missing_expression_text",
75 "Expression cost curves require expression_text.",
76 context={"curve_key": curve.curve_key},
77 )
78 formula_specs = _formula_input_specs(curve)
79 expression = parse_cost_expression(
80 expression_text,
81 variable_symbols=[spec.variable_symbol for spec in formula_specs],
82 )
83 return EconomicsFormula(
84 key=f"cost_curve:{curve.pk or curve.curve_key}",
85 expression=expression,
86 unit=curve.output_unit,
87 inputs=tuple(
88 FormulaInput(
89 key=spec.variable_symbol,
90 label=spec.label,
91 unit=spec.unit,
92 )
93 for spec in formula_specs
94 ),
95 )
98def render_cost_curve_formula(curve, *, formula_variable: str) -> str:
99 formula = build_cost_curve_formula(curve)
100 primary_input = next((formula_input for formula_input in formula.inputs), None)
101 if primary_input is None:
102 raise FormulaError(
103 "missing_formula_variables",
104 "Cost curve formula has no declared formula variables.",
105 context={"curve_key": curve.curve_key},
106 )
107 return formula.render_property_formula({primary_input.key: formula_variable})
110def build_cost_curve_variant_formula(curve, variant, *, formula_specs) -> EconomicsFormula:
111 """Build one formula candidate from a discrete-family variant expression."""
112 expression = parse_cost_expression(
113 variant.expression_text,
114 variable_symbols=[spec.variable_symbol for spec in formula_specs],
115 )
116 return EconomicsFormula(
117 key=f"cost_curve_variant:{curve.pk or curve.curve_key}:{variant.key}",
118 expression=expression,
119 unit=curve.output_unit,
120 inputs=tuple(
121 FormulaInput(
122 key=spec.variable_symbol,
123 label=spec.label,
124 unit=spec.unit,
125 )
126 for spec in formula_specs
127 ),
128 )
131def build_generated_capital_line_formula(mapping, driver=None, existing_line=None) -> GeneratedCapitalLineFormula:
132 """Compose curve, index, Lang-factor, and contingency arithmetic once."""
133 del driver, existing_line
134 curve = mapping.cost_curve
135 if curve is None: 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true
136 raise FormulaError(
137 "missing_cost_curve",
138 "Generated capital line formulas require a selected cost curve.",
139 context={"costable_item_id": mapping.costable_item_id},
140 )
141 curve_formula = (
142 build_cost_curve_formula(curve)
143 if curve.evaluation_kind == CostCurveEvaluationKind.EXPRESSION
144 else _placeholder_discrete_curve_formula(curve)
145 )
146 index_adjustment = capital_index_adjustment(mapping.costable_item.study, curve)
147 lang_factor_resolution = resolve_lang_factor(mapping)
148 applies_lang_factor = (
149 curve.cost_basis == CostBasis.PURCHASE
150 and lang_factor_resolution.effective_value is not None
151 )
152 contingency_percent = contingency_percent_for_study(mapping.costable_item.study) or Decimal("0")
153 contingency_factor = Decimal("1") + (contingency_percent / Decimal("100"))
155 expression = curve_formula.expression
156 steps = [
157 FormulaStep(
158 kind="base_curve_cost",
159 label="Curve base cost",
160 expression=curve_formula.audit_payload()["formula"],
161 unit=curve.output_unit,
162 )
163 ]
164 expression = _multiply_factor(
165 expression,
166 index_adjustment.factor,
167 steps=steps,
168 kind="index_adjustment",
169 label="CPI/index adjustment",
170 detail=index_adjustment.detail,
171 )
172 if applies_lang_factor: 172 ↛ 181line 172 didn't jump to line 181 because the condition on line 172 was always true
173 expression = _multiply_factor(
174 expression,
175 lang_factor_resolution.effective_value,
176 steps=steps,
177 kind="lang_factor",
178 label="Lang factor",
179 detail=lang_factor_resolution.source,
180 )
181 expression = _multiply_factor(
182 expression,
183 contingency_factor,
184 steps=steps,
185 kind="contingency",
186 label="Contingency",
187 percent=contingency_percent,
188 )
189 return GeneratedCapitalLineFormula(
190 formula=EconomicsFormula(
191 key=f"generated_capital_line:{mapping.pk}",
192 expression=expression,
193 unit=curve.output_unit,
194 inputs=curve_formula.inputs,
195 steps=tuple(steps),
196 ),
197 index_adjustment=index_adjustment,
198 lang_factor=lang_factor_resolution.effective_value,
199 lang_factor_source=lang_factor_resolution.source,
200 applies_lang_factor=applies_lang_factor,
201 contingency_percent=contingency_percent,
202 contingency_factor=contingency_factor,
203 )
206def _formula_input_specs(curve):
207 from Economics.costing.cost_curves.driver_specs import parse_required_driver_specs
209 try:
210 specs = parse_required_driver_specs(curve.required_driver_specs)
211 except ValueError as exc:
212 raise FormulaError(
213 "invalid_cost_curve_driver_specs",
214 str(exc),
215 context={"curve_key": curve.curve_key},
216 ) from exc
217 return tuple(spec for spec in specs if spec.role == "formula_input")
220def _placeholder_discrete_curve_formula(curve) -> EconomicsFormula:
221 """Provide factor steps for discrete curves before runtime variant selection.
223 Generated capital recalculation evaluates discrete candidates in
224 ``evaluate_cost_curve``. This placeholder keeps index/Lang/contingency
225 composition centralized without pretending there is a top-level expression.
226 """
227 return EconomicsFormula(
228 key=f"cost_curve:{curve.pk or curve.curve_key}",
229 expression=decimal_to_sympy(Decimal("1")),
230 unit=curve.output_unit,
231 inputs=(),
232 solve_visible=False,
233 )
236def build_generated_unit_capex_subtotal_formula(study) -> BoundCapitalFormula:
237 """Build the generated unit-operation CAPEX subtotal formula."""
238 terms: list[sympy.Expr] = []
239 blocked_children: list[dict[str, str]] = []
240 for line in (
241 study.capital_lines.filter(
242 included=True,
243 source=GENERATED_CAPITAL_LINE_SOURCE,
244 )
245 .exclude(costable_item__simulation_object__is_deleted=True)
246 .order_by("pk")
247 ):
248 if line.amount is None and line.cost_curve_id is not None:
249 blocked_children.append(
250 {
251 "key": f"capital_line:{line.pk}",
252 "reason": "included generated capital line has no amount",
253 }
254 )
255 continue
256 if line.amount is None:
257 continue
258 terms.append(decimal_to_sympy(line.amount))
259 return BoundCapitalFormula(
260 formula=EconomicsFormula(
261 key="generated_unit_capex_subtotal",
262 expression=_sum_expressions(terms),
263 unit=_study_currency(study),
264 inputs=(),
265 steps=(
266 FormulaStep(
267 kind="generated_capital_line_sum",
268 label="Generated unit CAPEX subtotal",
269 expression="sum(included generated capital lines)",
270 amount=None,
271 unit=_study_currency(study),
272 ),
273 ),
274 missing_child_policy="strict_included_children",
275 blocked_children=tuple(blocked_children),
276 blocked_reason="Included generated capital lines are missing amounts."
277 if blocked_children
278 else "",
279 ),
280 bindings={},
281 )
284def build_custom_capital_line_formula(line, *, base_capex: Decimal) -> BoundCapitalFormula:
285 """Build a formula for a fixed or percentage custom capital line."""
286 currency = line.currency or _study_currency(line.study)
287 if line.calculation_basis != CapitalLineBasis.BASE_CAPEX_PERCENT:
288 if line.amount is None: 288 ↛ 289line 288 didn't jump to line 289 because the condition on line 288 was never true
289 raise FormulaError(
290 "missing_custom_capital_amount",
291 "Fixed custom capital lines require an amount.",
292 context={"capital_line_id": line.pk},
293 )
294 return BoundCapitalFormula(
295 formula=EconomicsFormula(
296 key=f"custom_capital_line:{line.pk or 'unsaved'}",
297 expression=decimal_to_sympy(line.amount),
298 unit=currency,
299 inputs=(),
300 steps=(
301 FormulaStep(
302 kind="fixed_capital_amount",
303 label="Fixed capital amount",
304 expression=str(line.amount),
305 amount=line.amount,
306 unit=currency,
307 ),
308 ),
309 ),
310 bindings={},
311 )
312 if line.basis_percent is None: 312 ↛ 313line 312 didn't jump to line 313 because the condition on line 312 was never true
313 raise FormulaError(
314 "missing_custom_capital_percent",
315 "Percentage custom capital lines require a basis percent.",
316 context={"capital_line_id": line.pk},
317 )
318 basis_symbol = sympy.Symbol(CUSTOM_CAPEX_PERCENTAGE_BASIS)
319 percent_factor = line.basis_percent / Decimal("100")
320 return BoundCapitalFormula(
321 formula=EconomicsFormula(
322 key=f"custom_capital_line:{line.pk or 'unsaved'}",
323 expression=sympy.Mul(basis_symbol, decimal_to_sympy(percent_factor), evaluate=False),
324 unit=currency,
325 inputs=(
326 FormulaInput(
327 key=CUSTOM_CAPEX_PERCENTAGE_BASIS,
328 label="Custom CAPEX percentage basis",
329 unit=currency,
330 ),
331 ),
332 steps=(
333 FormulaStep(
334 kind="base_capex_percent",
335 label="Base CAPEX percentage",
336 expression=str(percent_factor),
337 amount=percent_factor,
338 unit="factor",
339 ),
340 ),
341 ),
342 bindings={CUSTOM_CAPEX_PERCENTAGE_BASIS: base_capex},
343 )
346def build_custom_capital_total_formula(study, *, base_capex: Decimal | None = None) -> BoundCapitalFormula:
347 """Build the included custom capital total formula."""
348 generated_subtotal = base_capex
349 if generated_subtotal is None:
350 generated_subtotal_formula = build_generated_unit_capex_subtotal_formula(study)
351 generated_subtotal = generated_subtotal_formula.evaluate()
352 if generated_subtotal is None:
353 return BoundCapitalFormula(
354 formula=EconomicsFormula(
355 key="custom_capital_total",
356 expression=decimal_to_sympy(Decimal("0")),
357 unit=_study_currency(study),
358 inputs=(),
359 missing_child_policy="strict_included_children",
360 blocked_children=(
361 {
362 "key": generated_subtotal_formula.formula.key,
363 "reason": generated_subtotal_formula.formula.blocked_reason,
364 },
365 ),
366 blocked_reason="Custom capital total requires generated unit CAPEX subtotal.",
367 ),
368 bindings={},
369 )
370 terms: list[sympy.Expr] = []
371 bindings: dict[str, Decimal] = {}
372 inputs: list[FormulaInput] = []
373 blocked_children: list[dict[str, str]] = []
374 for line in study.capital_lines.filter(included=True).exclude(source=GENERATED_CAPITAL_LINE_SOURCE).order_by("pk"):
375 try:
376 line_formula = build_custom_capital_line_formula(line, base_capex=generated_subtotal)
377 amount = line_formula.evaluate()
378 except FormulaError as exc:
379 amount = None
380 blocked_children.append({"key": f"capital_line:{line.pk}", "reason": exc.message})
381 if amount is None:
382 continue
383 if line.calculation_basis == CapitalLineBasis.BASE_CAPEX_PERCENT:
384 bindings[CUSTOM_CAPEX_PERCENTAGE_BASIS] = generated_subtotal
385 if not any(formula_input.key == CUSTOM_CAPEX_PERCENTAGE_BASIS for formula_input in inputs):
386 inputs.extend(line_formula.formula.inputs)
387 terms.append(line_formula.formula.expression)
388 return BoundCapitalFormula(
389 formula=EconomicsFormula(
390 key="custom_capital_total",
391 expression=_sum_expressions(terms),
392 unit=_study_currency(study),
393 inputs=tuple(inputs),
394 steps=(
395 FormulaStep(
396 kind="custom_capital_total",
397 label="Custom capital total",
398 expression="sum(included custom capital line formulas)",
399 unit=_study_currency(study),
400 ),
401 ),
402 missing_child_policy="strict_included_children",
403 blocked_children=tuple(blocked_children),
404 blocked_reason="Included custom capital lines are blocked." if blocked_children else "",
405 ),
406 bindings=bindings,
407 )
410def build_peak_demand_formula(study) -> BoundCapitalFormula:
411 """Build the peak-demand capacity formula used for electrical upgrade CAPEX."""
412 peak_demand_basis = derive_peak_demand_basis(study)
413 quantity_kw = peak_demand_basis.quantity_kw or Decimal("0")
414 return BoundCapitalFormula(
415 formula=EconomicsFormula(
416 key="peak_demand_capacity",
417 expression=decimal_to_sympy(quantity_kw),
418 unit=peak_demand_basis.unit,
419 inputs=(),
420 steps=(
421 FormulaStep(
422 kind="peak_demand_sum",
423 label="Peak demand capacity",
424 expression="sum(included capital line peak demand)",
425 amount=quantity_kw,
426 unit=peak_demand_basis.unit,
427 ),
428 ),
429 missing_child_policy="omitted_before_formula_when_not_applicable",
430 ),
431 bindings={},
432 )
435def build_electrical_upgrade_formula(study) -> BoundCapitalFormula:
436 """Build the electrical-upgrade CAPEX formula from power capacity and rate."""
437 assumptions = get_settings_profile(study)
438 if assumptions is None:
439 rate_amount = Decimal("0")
440 currency = "NZD"
441 else:
442 rate_amount = assumptions.electrical_upgrade_rate_amount or Decimal("0")
443 currency = assumptions.currency or "NZD"
444 peak_symbol = sympy.Symbol(PEAK_DEMAND_BASIS_KW)
445 rate_symbol = sympy.Symbol(ELECTRICAL_UPGRADE_RATE)
446 return BoundCapitalFormula(
447 formula=EconomicsFormula(
448 key="electrical_upgrade_capex",
449 expression=sympy.Mul(peak_symbol, rate_symbol, evaluate=False),
450 unit=currency,
451 inputs=(
452 FormulaInput(
453 key=PEAK_DEMAND_BASIS_KW,
454 label="Peak demand capacity",
455 unit="kW",
456 ),
457 FormulaInput(
458 key=ELECTRICAL_UPGRADE_RATE,
459 label="Electrical upgrade rate",
460 unit=f"{currency}/kW",
461 ),
462 ),
463 steps=(
464 FormulaStep(
465 kind="electrical_upgrade_rate",
466 label="Electrical upgrade rate",
467 expression=str(rate_amount),
468 amount=rate_amount,
469 unit=f"{currency}/kW",
470 ),
471 ),
472 ),
473 bindings={
474 PEAK_DEMAND_BASIS_KW: build_peak_demand_formula(study).evaluate() or Decimal("0"),
475 ELECTRICAL_UPGRADE_RATE: rate_amount,
476 },
477 )
480def build_target_total_capex_formula(study) -> BoundCapitalFormula:
481 """Build the target total CAPEX formula from included capital-line formulas."""
482 blocked_children: list[dict[str, str]] = []
483 terms: list[sympy.Expr] = []
484 inputs: list[FormulaInput] = []
485 bindings: dict[str, Decimal] = {}
486 generated_subtotal = build_generated_unit_capex_subtotal_formula(study).evaluate() or Decimal("0")
487 for line in study.capital_lines.filter(included=True).order_by("pk"):
488 amount = _capital_line_amount_for_total(line, generated_subtotal=generated_subtotal)
489 if amount is None:
490 if line.cost_curve_id is not None:
491 blocked_children.append(
492 {
493 "key": f"capital_line:{line.pk}",
494 "reason": "included capital line has no amount",
495 }
496 )
497 continue
498 symbol_key = capital_line_input_key(line.pk)
499 terms.append(sympy.Symbol(symbol_key))
500 inputs.append(
501 FormulaInput(
502 key=symbol_key,
503 label=line.label,
504 unit=_study_currency(study),
505 )
506 )
507 bindings[symbol_key] = amount
508 electrical_formula = build_electrical_upgrade_formula(study)
509 electrical_upgrade = electrical_formula.evaluate()
510 if electrical_upgrade is None: 510 ↛ 511line 510 didn't jump to line 511 because the condition on line 510 was never true
511 blocked_children.append({"key": electrical_formula.formula.key, "reason": electrical_formula.formula.blocked_reason})
512 electrical_upgrade = Decimal("0")
513 terms.append(sympy.Symbol(ELECTRICAL_UPGRADE_CAPEX))
514 inputs.append(
515 FormulaInput(
516 key=ELECTRICAL_UPGRADE_CAPEX,
517 label="Electrical upgrade CAPEX",
518 unit=_study_currency(study),
519 )
520 )
521 bindings[ELECTRICAL_UPGRADE_CAPEX] = electrical_upgrade
522 return BoundCapitalFormula(
523 formula=EconomicsFormula(
524 key="target_total_capex",
525 expression=_sum_expressions(terms),
526 unit=_study_currency(study),
527 inputs=tuple(inputs),
528 steps=(
529 FormulaStep(
530 kind="target_total_capex",
531 label="Target total CAPEX",
532 expression="generated subtotal + custom capital + electrical upgrade",
533 unit=_study_currency(study),
534 ),
535 ),
536 missing_child_policy="strict_included_children",
537 blocked_children=tuple(blocked_children),
538 blocked_reason="Target total CAPEX has blocked children." if blocked_children else "",
539 ),
540 bindings=bindings,
541 )
544def capital_line_input_key(line_id: int) -> str:
545 return f"capital_line_{line_id}"
548def _capital_line_amount_for_total(line, *, generated_subtotal: Decimal) -> Decimal | None:
549 if line.source == GENERATED_CAPITAL_LINE_SOURCE:
550 return line.amount
551 try:
552 return build_custom_capital_line_formula(line, base_capex=generated_subtotal).evaluate()
553 except FormulaError:
554 return None
557def capital_index_adjustment(study, curve) -> CapitalIndexAdjustment:
558 """Resolve the capital index factor applied to a curve-backed cost."""
559 assumptions = get_settings_profile(study)
560 if assumptions is None or assumptions.capital_index_series_id is None:
561 return CapitalIndexAdjustment(
562 factor=Decimal("1"),
563 detail="No capital index series selected",
564 )
565 target_value = _capital_index_target_value(assumptions)
566 if target_value is None:
567 raise FormulaError(
568 "missing_capital_index_target_value",
569 "Selected capital index series has no target value for the study basis date.",
570 context={
571 "study_id": study.pk,
572 "series_id": assumptions.capital_index_series_id,
573 "basis_date": None if assumptions.basis_date is None else assumptions.basis_date.isoformat(),
574 },
575 )
576 basis_value, basis_label = _capital_index_basis_value(assumptions, curve)
577 if basis_value in (None, Decimal("0")): 577 ↛ 578line 577 didn't jump to line 578 because the condition on line 577 was never true
578 raise FormulaError(
579 "missing_capital_index_basis_value",
580 "Selected capital index series has no curve basis value.",
581 context={
582 "curve_key": curve.curve_key,
583 "series_id": assumptions.capital_index_series_id,
584 "basis": basis_label,
585 },
586 )
587 factor = target_value.value / basis_value
588 return CapitalIndexAdjustment(
589 factor=factor,
590 detail=(
591 f"{target_value.series.name}: {target_value.period} "
592 f"{target_value.value} / {basis_label} {basis_value}"
593 ),
594 )
597def contingency_percent_for_study(study) -> Decimal | None:
598 """Return the study contingency percentage applied to generated capital costs."""
599 assumptions = get_settings_profile(study)
600 if assumptions is None or assumptions.contingency_percent is None: 600 ↛ 601line 600 didn't jump to line 601 because the condition on line 600 was never true
601 return None
602 return assumptions.contingency_percent
605def _multiply_factor(
606 expression: sympy.Expr,
607 factor: Decimal,
608 *,
609 steps: list[FormulaStep],
610 kind: str,
611 label: str,
612 detail: str = "",
613 percent: Decimal | None = None,
614) -> sympy.Expr:
615 if factor == Decimal("1"):
616 return expression
617 steps.append(
618 FormulaStep(
619 kind=kind,
620 label=label,
621 expression=str(factor),
622 amount=factor,
623 unit="factor",
624 )
625 )
626 return sympy.Mul(expression, decimal_to_sympy(factor), evaluate=False)
629def _sum_expressions(terms: list[sympy.Expr]) -> sympy.Expr:
630 if not terms:
631 return decimal_to_sympy(Decimal("0"))
632 if len(terms) == 1:
633 return terms[0]
634 return sympy.Add(*terms, evaluate=False)
637def _study_currency(study) -> str:
638 assumptions = get_settings_profile(study)
639 if assumptions is None:
640 return "NZD"
641 return assumptions.currency or "NZD"
644def _capital_result_amount(value: Decimal) -> Decimal:
645 return value.quantize(CAPITAL_RESULT_QUANTUM, rounding=ROUND_HALF_UP)
648def _decimal_literal(value: Decimal | int | str) -> str:
649 decimal_value = Decimal(str(value))
650 return format(decimal_value, "f").rstrip("0").rstrip(".") or "0"
653def _capital_index_basis_value(assumptions: EconomicsSettingsProfile, curve) -> tuple[Decimal | None, str]:
654 """Resolve the curve-side index value used as the escalation denominator."""
655 if curve.basis_index_value is not None: 655 ↛ 656line 655 didn't jump to line 656 because the condition on line 655 was never true
656 basis_label = curve.basis_date.isoformat() if curve.basis_date else "curve basis"
657 return curve.basis_index_value, basis_label
658 if curve.basis_date is None: 658 ↛ 659line 658 didn't jump to line 659 because the condition on line 658 was never true
659 return None, "curve basis"
660 basis_value = (
661 assumptions.capital_index_series.values.filter(period_date__lte=curve.basis_date)
662 .order_by("-period_date", "-pk")
663 .first()
664 )
665 if basis_value is None: 665 ↛ 666line 665 didn't jump to line 666 because the condition on line 665 was never true
666 return None, curve.basis_date.isoformat()
667 return basis_value.value, basis_value.period
670def _capital_index_target_value(assumptions: EconomicsSettingsProfile) -> CostIndexValue | None:
671 """Resolve the latest selected index value at or before the study basis date."""
672 values = assumptions.capital_index_series.values.order_by("-period_date", "-pk")
673 if assumptions.basis_date is not None: 673 ↛ 675line 673 didn't jump to line 675 because the condition on line 673 was always true
674 values = values.filter(period_date__lte=assumptions.basis_date)
675 return values.first()