Coverage for backend/django/Economics/settings_profiles/services/settings_profiles.py: 75%

113 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-06-23 21:51 +0000

1from __future__ import annotations 

2 

3from datetime import date 

4from decimal import Decimal 

5from typing import Any, Iterable, TypedDict 

6 

7from django.core.exceptions import ObjectDoesNotExist 

8from django.db import transaction 

9 

10from core.auxiliary.models import Flowsheet 

11from Economics.reference_data.models import CostIndexSeries 

12from Economics.settings_profiles.models import EconomicsAssumptions, EconomicsBaseline, EconomicsSettingsProfile 

13from Economics.studies.models import EconomicsStudy 

14 

15 

16class SettingsProfileValues(TypedDict, total=False): 

17 currency: str 

18 location: str 

19 basis_date: date | None 

20 discount_rate_percent: Decimal | None 

21 project_lifetime_years: int | None 

22 inflation_method: str 

23 annual_operating_hours: Decimal | None 

24 tax_rate_percent: Decimal | None 

25 depreciation_enabled: bool 

26 default_depreciation_life_years: int | None 

27 default_depreciation_salvage_percent: Decimal | None 

28 contingency_percent: Decimal | None 

29 electrical_upgrade_rate_amount: Decimal | None 

30 default_lang_factor: Decimal 

31 capital_index_series: CostIndexSeries | None 

32 operating_index_series: CostIndexSeries | None 

33 default_rate_overrides: dict[str, Any] 

34 manual_capex: Decimal | None 

35 manual_annual_opex: Decimal | None 

36 annual_heat_basis_mode: str 

37 manual_annual_heat_basis: Decimal | None 

38 manual_annual_heat_basis_unit: str 

39 average_power_input: Decimal | None 

40 average_power_unit: str 

41 residual_value: Decimal | None 

42 notes: str 

43 baseline_notes: str 

44 

45 

46def get_settings_profile(study: EconomicsStudy) -> EconomicsSettingsProfile | None: 

47 """Return the active profile selected by a study, if configured.""" 

48 

49 try: 

50 return study.settings_profile 

51 except ObjectDoesNotExist: 

52 return None 

53 

54 

55def default_settings_profile_for_flowsheet(flowsheet: Flowsheet) -> EconomicsSettingsProfile | None: 

56 return ( 

57 EconomicsSettingsProfile.objects.filter(flowsheet=flowsheet, is_default=True) 

58 .order_by("created_at", "pk") 

59 .first() 

60 ) 

61 

62 

63def get_or_create_default_settings_profile(flowsheet: Flowsheet) -> EconomicsSettingsProfile: 

64 profile = default_settings_profile_for_flowsheet(flowsheet) 

65 if profile is not None: 

66 return profile 

67 return EconomicsSettingsProfile.objects.create( 

68 flowsheet=flowsheet, 

69 name="Default study profile", 

70 is_default=True, 

71 ) 

72 

73 

74@transaction.atomic 

75def create_settings_profile_copy( 

76 *, 

77 source: EconomicsSettingsProfile, 

78 name: str, 

79 is_default: bool = False, 

80) -> EconomicsSettingsProfile: 

81 """Copy a settings profile inside the same flowsheet with a new name.""" 

82 

83 if is_default: 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true

84 EconomicsSettingsProfile.objects.filter(flowsheet=source.flowsheet, is_default=True).update(is_default=False) 

85 values = _profile_values(source) 

86 return EconomicsSettingsProfile.objects.create( 

87 flowsheet=source.flowsheet, 

88 name=name, 

89 is_default=is_default, 

90 **values, 

91 ) 

92 

93 

94def settings_profile_from_legacy_study(study: EconomicsStudy, *, name: str, is_default: bool) -> EconomicsSettingsProfile: 

95 """Build a profile from the former per-study assumptions/baseline rows.""" 

96 

97 values: SettingsProfileValues = {} 

98 try: 

99 assumptions = study.assumptions 

100 except ObjectDoesNotExist: 

101 assumptions = None 

102 if assumptions is not None: 

103 values.update(_assumption_values(assumptions)) 

104 

105 try: 

106 baseline = study.baseline 

107 except ObjectDoesNotExist: 

108 baseline = None 

109 if baseline is not None: 

110 values.update(_baseline_values(baseline)) 

111 

112 return EconomicsSettingsProfile.objects.create( 

113 flowsheet=study.flowsheet, 

114 name=name, 

115 is_default=is_default, 

116 **values, 

117 ) 

118 

119 

120@transaction.atomic 

121def update_settings_profile_from_assumptions( 

122 assumptions: EconomicsAssumptions, 

123 *, 

124 field_names: Iterable[str] | None = None, 

125) -> EconomicsSettingsProfile: 

126 """Apply legacy assumptions endpoint values to the study's selected profile.""" 

127 

128 return _update_settings_profile( 

129 _profile_for_legacy_endpoint_update(assumptions.study), 

130 _filtered_legacy_values(_assumption_values(assumptions), field_names), 

131 ) 

132 

133 

134@transaction.atomic 

135def update_settings_profile_from_baseline( 

136 baseline: EconomicsBaseline, 

137 *, 

138 field_names: Iterable[str] | None = None, 

139) -> EconomicsSettingsProfile: 

140 """Apply legacy baseline endpoint values to the study's selected profile.""" 

141 

142 return _update_settings_profile( 

143 _profile_for_legacy_endpoint_update(baseline.study), 

144 _filtered_legacy_values( 

145 _baseline_values(baseline), 

146 field_names, 

147 field_map={"notes": "baseline_notes"}, 

148 ), 

149 ) 

150 

151 

152def assumptions_from_settings_profile(study: EconomicsStudy) -> EconomicsAssumptions | None: 

153 """Return an unsaved legacy assumptions row for serializers backed by the selected profile.""" 

154 

155 profile = get_settings_profile(study) 

156 if profile is None: 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true

157 return None 

158 return EconomicsAssumptions( 

159 flowsheet=study.flowsheet, 

160 study=study, 

161 currency=profile.currency, 

162 location=profile.location, 

163 basis_date=profile.basis_date, 

164 discount_rate_percent=profile.discount_rate_percent, 

165 project_lifetime_years=profile.project_lifetime_years, 

166 inflation_method=profile.inflation_method, 

167 annual_operating_hours=profile.annual_operating_hours, 

168 tax_rate_percent=profile.tax_rate_percent, 

169 depreciation_enabled=profile.depreciation_enabled, 

170 default_depreciation_life_years=profile.default_depreciation_life_years, 

171 default_depreciation_salvage_percent=profile.default_depreciation_salvage_percent, 

172 contingency_percent=profile.contingency_percent, 

173 electrical_upgrade_rate_amount=profile.electrical_upgrade_rate_amount, 

174 default_lang_factor=profile.default_lang_factor, 

175 capital_index_series=profile.capital_index_series, 

176 operating_index_series=profile.operating_index_series, 

177 default_rate_overrides=profile.default_rate_overrides, 

178 notes=profile.notes, 

179 ) 

180 

181 

182def baseline_from_settings_profile(study: EconomicsStudy) -> EconomicsBaseline | None: 

183 """Return an unsaved legacy baseline row for serializers backed by the selected profile.""" 

184 

185 profile = get_settings_profile(study) 

186 if profile is None: 186 ↛ 187line 186 didn't jump to line 187 because the condition on line 186 was never true

187 return None 

188 return EconomicsBaseline( 

189 flowsheet=study.flowsheet, 

190 study=study, 

191 manual_capex=profile.manual_capex, 

192 manual_annual_opex=profile.manual_annual_opex, 

193 annual_heat_basis_mode=profile.annual_heat_basis_mode, 

194 manual_annual_heat_basis=profile.manual_annual_heat_basis, 

195 manual_annual_heat_basis_unit=profile.manual_annual_heat_basis_unit, 

196 average_power_input=profile.average_power_input, 

197 average_power_unit=profile.average_power_unit, 

198 residual_value=profile.residual_value, 

199 notes=profile.baseline_notes, 

200 ) 

201 

202 

203def _profile_for_legacy_endpoint_update(study: EconomicsStudy) -> EconomicsSettingsProfile: 

204 profile = get_settings_profile(study) 

205 if profile is not None: 205 ↛ 207line 205 didn't jump to line 207 because the condition on line 205 was always true

206 return profile 

207 profile = get_or_create_default_settings_profile(study.flowsheet) 

208 study.settings_profile = profile 

209 study.save(update_fields=["settings_profile"]) 

210 return profile 

211 

212 

213def _update_settings_profile( 

214 profile: EconomicsSettingsProfile, 

215 values: SettingsProfileValues, 

216) -> EconomicsSettingsProfile: 

217 if not values: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 return profile 

219 for field_name, value in values.items(): 

220 setattr(profile, field_name, value) 

221 profile.save(update_fields=list(values.keys())) 

222 return profile 

223 

224 

225def _filtered_legacy_values( 

226 values: SettingsProfileValues, 

227 field_names: Iterable[str] | None, 

228 *, 

229 field_map: dict[str, str] | None = None, 

230) -> SettingsProfileValues: 

231 if field_names is None: 

232 return values 

233 mapped_names = { 

234 (field_map or {}).get(field_name, field_name) 

235 for field_name in field_names 

236 if field_name not in {"study", "flowsheet"} 

237 } 

238 return { 

239 field_name: value 

240 for field_name, value in values.items() 

241 if field_name in mapped_names 

242 } 

243 

244 

245def _profile_values(profile: EconomicsSettingsProfile) -> SettingsProfileValues: 

246 return { 

247 "currency": profile.currency, 

248 "location": profile.location, 

249 "basis_date": profile.basis_date, 

250 "discount_rate_percent": profile.discount_rate_percent, 

251 "project_lifetime_years": profile.project_lifetime_years, 

252 "inflation_method": profile.inflation_method, 

253 "annual_operating_hours": profile.annual_operating_hours, 

254 "tax_rate_percent": profile.tax_rate_percent, 

255 "depreciation_enabled": profile.depreciation_enabled, 

256 "default_depreciation_life_years": profile.default_depreciation_life_years, 

257 "default_depreciation_salvage_percent": profile.default_depreciation_salvage_percent, 

258 "contingency_percent": profile.contingency_percent, 

259 "electrical_upgrade_rate_amount": profile.electrical_upgrade_rate_amount, 

260 "default_lang_factor": profile.default_lang_factor, 

261 "capital_index_series": profile.capital_index_series, 

262 "operating_index_series": profile.operating_index_series, 

263 "default_rate_overrides": profile.default_rate_overrides, 

264 "manual_capex": profile.manual_capex, 

265 "manual_annual_opex": profile.manual_annual_opex, 

266 "annual_heat_basis_mode": profile.annual_heat_basis_mode, 

267 "manual_annual_heat_basis": profile.manual_annual_heat_basis, 

268 "manual_annual_heat_basis_unit": profile.manual_annual_heat_basis_unit, 

269 "average_power_input": profile.average_power_input, 

270 "average_power_unit": profile.average_power_unit, 

271 "residual_value": profile.residual_value, 

272 "notes": profile.notes, 

273 "baseline_notes": profile.baseline_notes, 

274 } 

275 

276 

277def _assumption_values(assumptions: EconomicsAssumptions) -> SettingsProfileValues: 

278 return { 

279 "currency": assumptions.currency, 

280 "location": assumptions.location, 

281 "basis_date": assumptions.basis_date, 

282 "discount_rate_percent": assumptions.discount_rate_percent, 

283 "project_lifetime_years": assumptions.project_lifetime_years, 

284 "inflation_method": assumptions.inflation_method, 

285 "annual_operating_hours": assumptions.annual_operating_hours, 

286 "tax_rate_percent": assumptions.tax_rate_percent, 

287 "depreciation_enabled": assumptions.depreciation_enabled, 

288 "default_depreciation_life_years": assumptions.default_depreciation_life_years, 

289 "default_depreciation_salvage_percent": assumptions.default_depreciation_salvage_percent, 

290 "contingency_percent": assumptions.contingency_percent, 

291 "electrical_upgrade_rate_amount": assumptions.electrical_upgrade_rate_amount, 

292 "default_lang_factor": assumptions.default_lang_factor, 

293 "capital_index_series": assumptions.capital_index_series, 

294 "operating_index_series": assumptions.operating_index_series, 

295 "default_rate_overrides": assumptions.default_rate_overrides, 

296 "notes": assumptions.notes, 

297 } 

298 

299 

300def _baseline_values(baseline: EconomicsBaseline) -> SettingsProfileValues: 

301 return { 

302 "manual_capex": baseline.manual_capex, 

303 "manual_annual_opex": baseline.manual_annual_opex, 

304 "annual_heat_basis_mode": baseline.annual_heat_basis_mode, 

305 "manual_annual_heat_basis": baseline.manual_annual_heat_basis, 

306 "manual_annual_heat_basis_unit": baseline.manual_annual_heat_basis_unit, 

307 "average_power_input": baseline.average_power_input, 

308 "average_power_unit": baseline.average_power_unit, 

309 "residual_value": baseline.residual_value, 

310 "baseline_notes": baseline.notes, 

311 }