Coverage for backend/django/Economics/formulas/native_properties/sync.py: 97%
82 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
3import re
5from django.db import transaction
7from Economics.formulas.models import EconomicsMetricFormula
8from Economics.studies.models import EconomicsStudy
9from Economics.results.services.financial_metrics import FinancialMetricsError, calculate_study_financial_metrics
10from Economics.formulas.builders.native import native_property_expression
11from Economics.formulas.builders.native_property_formulas import NativePropertyExpression
12from Economics.formulas.property_state import apply_economics_property_state
13from Economics.costing.line_properties.sync import sync_economics_line_properties_for_study
14from Economics.formulas.native_properties.materialize import materialize_economics_native_properties
15from Economics.formulas.native_properties.specs import EconomicsNativePropertySpec, native_property_specs
17PROPERTY_MENTION_RE = re.compile(r"@\[[^\]]+\]\(prop(\d+)\)")
20def sync_economics_native_properties_for_study(study: EconomicsStudy) -> dict[str, int]:
21 """Materialize native properties and refresh display/formula fields."""
23 with transaction.atomic():
24 values = materialize_economics_native_properties(study)
25 specs_by_key = {spec.field_key: spec for spec in native_property_specs(study)}
26 sync_economics_line_properties_for_study(study)
27 synced: dict[str, int] = {}
28 solve_visible_reference_ids: set[int] = set()
29 blocked_reference_reasons: dict[int, str] = {}
30 financial_metrics = None
31 financial_metrics_error = None
32 if any(spec.result_metric_key for spec in specs_by_key.values()): 32 ↛ 40line 32 didn't jump to line 40 because the condition on line 32 was always true
33 try:
34 financial_metrics = calculate_study_financial_metrics(study)
35 except FinancialMetricsError as exc:
36 financial_metrics_error = exc
37 # Native property specs are emitted in dependency order. The reference
38 # guard below can therefore block formulas that compose from earlier
39 # generated properties that were not solve-visible.
40 for field_key, property_value in values.items():
41 expression = native_property_expression(
42 study,
43 field_key,
44 financial_metrics=financial_metrics,
45 financial_metrics_error=financial_metrics_error,
46 )
47 expression = _block_expression_with_invalid_references(
48 expression,
49 solve_visible_reference_ids=solve_visible_reference_ids,
50 blocked_reference_reasons=blocked_reference_reasons,
51 )
52 spec = specs_by_key[field_key]
53 solve_visible = expression.solve_visible and spec.solve_visible
54 display_value = None if expression.value is None else str(expression.value)
55 incomplete_reason = "" if solve_visible else (
56 expression.blocked_reason
57 or f"`{property_value.property.displayName}` is incomplete."
58 )
59 apply_economics_property_state(
60 property_value.property,
61 editable=False,
62 formula_incomplete=not solve_visible,
63 formula_incomplete_reason=incomplete_reason,
64 )
65 changed_fields = []
66 if property_value.value != display_value:
67 property_value.value = display_value
68 property_value.displayValue = display_value
69 changed_fields.extend(["value", "displayValue"])
70 if property_value.formula != expression.formula:
71 property_value.formula = expression.formula
72 changed_fields.append("formula")
73 if changed_fields:
74 property_value.save(update_fields=changed_fields)
75 if solve_visible:
76 solve_visible_reference_ids.add(property_value.pk)
77 blocked_reference_reasons.pop(property_value.pk, None)
78 else:
79 solve_visible_reference_ids.discard(property_value.pk)
80 blocked_reference_reasons[property_value.pk] = (
81 expression.blocked_reason
82 or f"`{property_value.property.displayName}` is not solve-visible."
83 )
84 formula_record_id = expression.formula_record_id
85 metric_key = spec.result_metric_key
86 if formula_record_id is None and financial_metrics is not None and metric_key:
87 metric = financial_metrics.metrics.get(metric_key)
88 formula_record_id = metric.formula_record_id if metric is not None else None
89 _link_native_formula_property(
90 study=study,
91 spec=spec,
92 property_value=property_value,
93 expression=expression,
94 formula_record_id=formula_record_id,
95 solve_visible=solve_visible,
96 )
97 synced[field_key] = property_value.pk
98 return synced
101def _link_native_formula_property(
102 *,
103 study: EconomicsStudy,
104 spec: EconomicsNativePropertySpec,
105 property_value,
106 expression: NativePropertyExpression,
107 formula_record_id: int | None,
108 solve_visible: bool,
109) -> None:
110 if formula_record_id is not None:
111 EconomicsMetricFormula.objects.filter(
112 pk=formula_record_id,
113 flowsheet=study.flowsheet,
114 study=study,
115 ).update(property_value=property_value)
116 return
118 status = "calculated" if expression.value is not None and solve_visible else "unavailable"
119 EconomicsMetricFormula.objects.update_or_create(
120 flowsheet=study.flowsheet,
121 study=study,
122 metric_key=_native_metric_key(spec),
123 defaults={
124 "property_value": property_value,
125 "formula_key": spec.field_key,
126 "formula": expression.formula,
127 "property_formula": expression.formula,
128 "unit": property_value.property.unit,
129 "value": str(expression.value) if expression.value is not None else None,
130 "status": status,
131 "formula_audit": {
132 "formula_key": spec.field_key,
133 "formula": expression.formula,
134 "value": str(expression.value) if expression.value is not None else None,
135 },
136 "blocked_reason": "" if solve_visible else expression.blocked_reason,
137 },
138 )
141def _native_metric_key(spec: EconomicsNativePropertySpec) -> str:
142 return spec.result_metric_key or spec.field_key
145def _block_expression_with_invalid_references(
146 expression: NativePropertyExpression,
147 *,
148 solve_visible_reference_ids: set[int],
149 blocked_reference_reasons: dict[int, str],
150) -> NativePropertyExpression:
151 """Block generated formulas that reference blocked generated children."""
153 if not expression.solve_visible or not expression.formula:
154 return expression
155 reference_ids = sorted(
156 {int(match.group(1)) for match in PROPERTY_MENTION_RE.finditer(expression.formula)}
157 )
158 if not reference_ids:
159 return expression
160 for reference_id in reference_ids:
161 if reference_id in solve_visible_reference_ids:
162 continue
163 reason = blocked_reference_reasons.get(reference_id)
164 if reason:
165 return _blocked_reference_expression(expression, reason)
166 return expression
169def _blocked_reference_expression(
170 expression: NativePropertyExpression,
171 reason: str,
172) -> NativePropertyExpression:
173 return NativePropertyExpression(
174 "",
175 False,
176 reason,
177 expression.value,
178 expression.formula_record_id,
179 )