Coverage for backend/django/Economics/settings_profiles/services/settings_profiles.py: 82%
107 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1from __future__ import annotations
3from datetime import date
4from decimal import Decimal
5from typing import Any, Iterable, TypedDict
7from django.core.exceptions import ObjectDoesNotExist
8from django.db import IntegrityError, transaction
10from core.auxiliary.models import Project
11from Economics.reference_data.models import CostIndexSeries
12from Economics.settings_profiles.defaults import apply_new_profile_defaults
13from Economics.settings_profiles.models import EconomicsAssumptions, EconomicsBaseline, EconomicsSettingsProfile
14from Economics.studies.models import EconomicsStudy
17class SettingsProfileValues(TypedDict, total=False):
18 currency: str
19 location: str
20 basis_date: date | None
21 discount_rate_percent: Decimal | None
22 project_lifetime_years: int | None
23 inflation_method: str
24 annual_operating_hours: Decimal | None
25 tax_rate_percent: Decimal | None
26 depreciation_enabled: bool
27 default_depreciation_life_years: int | None
28 default_depreciation_salvage_percent: Decimal | None
29 contingency_percent: Decimal | None
30 electrical_upgrade_rate_amount: Decimal | None
31 default_lang_factor: Decimal
32 capital_index_series: CostIndexSeries | None
33 operating_index_series: CostIndexSeries | None
34 default_rate_overrides: dict[str, Any]
35 manual_capex: Decimal | None
36 manual_annual_opex: Decimal | None
37 annual_heat_basis_mode: str
38 manual_annual_heat_basis: Decimal | None
39 manual_annual_heat_basis_unit: str
40 average_power_input: Decimal | None
41 average_power_unit: str
42 residual_value: Decimal | None
43 notes: str
44 baseline_notes: str
47def get_settings_profile(study: EconomicsStudy) -> EconomicsSettingsProfile | None:
48 """Return the active profile selected by a study, if configured."""
50 try:
51 return study.settings_profile
52 except ObjectDoesNotExist:
53 return None
56def default_settings_profile_for_project(project: Project) -> EconomicsSettingsProfile | None:
57 return (
58 EconomicsSettingsProfile._base_manager.filter(project=project, is_default=True)
59 .order_by("created_at", "pk")
60 .first()
61 )
64def get_or_create_default_settings_profile_for_project(project: Project) -> EconomicsSettingsProfile:
65 profile = default_settings_profile_for_project(project)
66 if profile is not None:
67 return profile
68 values = {
69 "project": project,
70 "name": "Default study profile",
71 "is_default": True,
72 }
73 apply_new_profile_defaults(values)
74 try:
75 return EconomicsSettingsProfile._base_manager.create(**values)
76 except IntegrityError:
77 profile = default_settings_profile_for_project(project)
78 if profile is None:
79 raise
80 return profile
83@transaction.atomic
84def create_settings_profile_copy(
85 *,
86 source: EconomicsSettingsProfile,
87 name: str,
88 is_default: bool = False,
89) -> EconomicsSettingsProfile:
90 """Copy a settings profile inside the same project with a new name."""
92 if is_default: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 EconomicsSettingsProfile._base_manager.filter(project=source.project, is_default=True).update(is_default=False)
94 values = _profile_values(source)
95 return EconomicsSettingsProfile._base_manager.create(
96 project=source.project,
97 name=name,
98 is_default=is_default,
99 **values,
100 )
103@transaction.atomic
104def update_settings_profile_from_assumptions(
105 assumptions: EconomicsAssumptions,
106 *,
107 field_names: Iterable[str] | None = None,
108) -> EconomicsSettingsProfile:
109 """Apply legacy assumptions endpoint values to the study's selected profile."""
111 return _update_settings_profile(
112 _profile_for_legacy_endpoint_update(assumptions.study),
113 _filtered_legacy_values(_assumption_values(assumptions), field_names),
114 )
117@transaction.atomic
118def update_settings_profile_from_baseline(
119 baseline: EconomicsBaseline,
120 *,
121 field_names: Iterable[str] | None = None,
122) -> EconomicsSettingsProfile:
123 """Apply legacy baseline endpoint values to the study's selected profile."""
125 return _update_settings_profile(
126 _profile_for_legacy_endpoint_update(baseline.study),
127 _filtered_legacy_values(
128 _baseline_values(baseline),
129 field_names,
130 field_map={"notes": "baseline_notes"},
131 ),
132 )
135def assumptions_from_settings_profile(study: EconomicsStudy) -> EconomicsAssumptions | None:
136 """Return an unsaved legacy assumptions row for serializers backed by the selected profile."""
138 profile = get_settings_profile(study)
139 if profile is None: 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 return None
141 return EconomicsAssumptions(
142 flowsheet_state=study.flowsheet_state,
143 study=study,
144 currency=profile.currency,
145 location=profile.location,
146 basis_date=profile.basis_date,
147 discount_rate_percent=profile.discount_rate_percent,
148 project_lifetime_years=profile.project_lifetime_years,
149 inflation_method=profile.inflation_method,
150 annual_operating_hours=profile.annual_operating_hours,
151 tax_rate_percent=profile.tax_rate_percent,
152 depreciation_enabled=profile.depreciation_enabled,
153 default_depreciation_life_years=profile.default_depreciation_life_years,
154 default_depreciation_salvage_percent=profile.default_depreciation_salvage_percent,
155 contingency_percent=profile.contingency_percent,
156 electrical_upgrade_rate_amount=profile.electrical_upgrade_rate_amount,
157 default_lang_factor=profile.default_lang_factor,
158 capital_index_series=profile.capital_index_series,
159 operating_index_series=profile.operating_index_series,
160 default_rate_overrides=profile.default_rate_overrides,
161 notes=profile.notes,
162 )
165def baseline_from_settings_profile(study: EconomicsStudy) -> EconomicsBaseline | None:
166 """Return an unsaved legacy baseline row for serializers backed by the selected profile."""
168 profile = get_settings_profile(study)
169 if profile is None: 169 ↛ 170line 169 didn't jump to line 170 because the condition on line 169 was never true
170 return None
171 return EconomicsBaseline(
172 flowsheet_state=study.flowsheet_state,
173 study=study,
174 manual_capex=profile.manual_capex,
175 manual_annual_opex=profile.manual_annual_opex,
176 annual_heat_basis_mode=profile.annual_heat_basis_mode,
177 manual_annual_heat_basis=profile.manual_annual_heat_basis,
178 manual_annual_heat_basis_unit=profile.manual_annual_heat_basis_unit,
179 average_power_input=profile.average_power_input,
180 average_power_unit=profile.average_power_unit,
181 residual_value=profile.residual_value,
182 notes=profile.baseline_notes,
183 )
186def _profile_for_legacy_endpoint_update(study: EconomicsStudy) -> EconomicsSettingsProfile:
187 profile = get_settings_profile(study)
188 if profile is not None: 188 ↛ 190line 188 didn't jump to line 190 because the condition on line 188 was always true
189 return profile
190 profile = get_or_create_default_settings_profile_for_project(study.flowsheet_state.flowsheet.project)
191 study.settings_profile = profile
192 study.save(update_fields=["settings_profile"])
193 return profile
196def _update_settings_profile(
197 profile: EconomicsSettingsProfile,
198 values: SettingsProfileValues,
199) -> EconomicsSettingsProfile:
200 if not values: 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true
201 return profile
202 for field_name, value in values.items():
203 setattr(profile, field_name, value)
204 profile.save(update_fields=list(values.keys()))
205 return profile
208def _filtered_legacy_values(
209 values: SettingsProfileValues,
210 field_names: Iterable[str] | None,
211 *,
212 field_map: dict[str, str] | None = None,
213) -> SettingsProfileValues:
214 if field_names is None:
215 return values
216 mapped_names = {
217 (field_map or {}).get(field_name, field_name)
218 for field_name in field_names
219 if field_name not in {"study", "flowsheet"}
220 }
221 return {
222 field_name: value
223 for field_name, value in values.items()
224 if field_name in mapped_names
225 }
228def _profile_values(profile: EconomicsSettingsProfile) -> SettingsProfileValues:
229 return {
230 "currency": profile.currency,
231 "location": profile.location,
232 "basis_date": profile.basis_date,
233 "discount_rate_percent": profile.discount_rate_percent,
234 "project_lifetime_years": profile.project_lifetime_years,
235 "inflation_method": profile.inflation_method,
236 "annual_operating_hours": profile.annual_operating_hours,
237 "tax_rate_percent": profile.tax_rate_percent,
238 "depreciation_enabled": profile.depreciation_enabled,
239 "default_depreciation_life_years": profile.default_depreciation_life_years,
240 "default_depreciation_salvage_percent": profile.default_depreciation_salvage_percent,
241 "contingency_percent": profile.contingency_percent,
242 "electrical_upgrade_rate_amount": profile.electrical_upgrade_rate_amount,
243 "default_lang_factor": profile.default_lang_factor,
244 "capital_index_series": profile.capital_index_series,
245 "operating_index_series": profile.operating_index_series,
246 "default_rate_overrides": profile.default_rate_overrides,
247 "manual_capex": profile.manual_capex,
248 "manual_annual_opex": profile.manual_annual_opex,
249 "annual_heat_basis_mode": profile.annual_heat_basis_mode,
250 "manual_annual_heat_basis": profile.manual_annual_heat_basis,
251 "manual_annual_heat_basis_unit": profile.manual_annual_heat_basis_unit,
252 "average_power_input": profile.average_power_input,
253 "average_power_unit": profile.average_power_unit,
254 "residual_value": profile.residual_value,
255 "notes": profile.notes,
256 "baseline_notes": profile.baseline_notes,
257 }
260def _assumption_values(assumptions: EconomicsAssumptions) -> SettingsProfileValues:
261 return {
262 "currency": assumptions.currency,
263 "location": assumptions.location,
264 "basis_date": assumptions.basis_date,
265 "discount_rate_percent": assumptions.discount_rate_percent,
266 "project_lifetime_years": assumptions.project_lifetime_years,
267 "inflation_method": assumptions.inflation_method,
268 "annual_operating_hours": assumptions.annual_operating_hours,
269 "tax_rate_percent": assumptions.tax_rate_percent,
270 "depreciation_enabled": assumptions.depreciation_enabled,
271 "default_depreciation_life_years": assumptions.default_depreciation_life_years,
272 "default_depreciation_salvage_percent": assumptions.default_depreciation_salvage_percent,
273 "contingency_percent": assumptions.contingency_percent,
274 "electrical_upgrade_rate_amount": assumptions.electrical_upgrade_rate_amount,
275 "default_lang_factor": assumptions.default_lang_factor,
276 "capital_index_series": assumptions.capital_index_series,
277 "operating_index_series": assumptions.operating_index_series,
278 "default_rate_overrides": assumptions.default_rate_overrides,
279 "notes": assumptions.notes,
280 }
283def _baseline_values(baseline: EconomicsBaseline) -> SettingsProfileValues:
284 return {
285 "manual_capex": baseline.manual_capex,
286 "manual_annual_opex": baseline.manual_annual_opex,
287 "annual_heat_basis_mode": baseline.annual_heat_basis_mode,
288 "manual_annual_heat_basis": baseline.manual_annual_heat_basis,
289 "manual_annual_heat_basis_unit": baseline.manual_annual_heat_basis_unit,
290 "average_power_input": baseline.average_power_input,
291 "average_power_unit": baseline.average_power_unit,
292 "residual_value": baseline.residual_value,
293 "baseline_notes": baseline.notes,
294 }