Coverage for backend/django/Economics/costing/line_properties/sync.py: 85%

152 statements  

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

1from __future__ import annotations 

2 

3from decimal import Decimal 

4 

5from core.auxiliary.models.PropertyInfo import PropertyInfo 

6from core.auxiliary.models.PropertySet import PropertySet 

7from core.auxiliary.models.PropertyValue import PropertyValue 

8from django.core.exceptions import ObjectDoesNotExist 

9from django.db import transaction 

10from Economics.costing.models import CapitalCostLine, OperatingCostLine 

11from Economics.shared.choices import CapitalLineBasis 

12from Economics.formulas.models import EconomicsLineFormula 

13from Economics.formulas.property_state import apply_economics_property_state 

14from Economics.studies.models import EconomicsStudy 

15from Economics.settings_profiles.services.settings_profiles import get_settings_profile 

16from Economics.costing.capital.capital_line_sources import GENERATED_CAPITAL_LINE_SOURCE 

17from Economics.formulas.builders.capital import build_custom_capital_line_formula, build_generated_unit_capex_subtotal_formula 

18from Economics.formulas.engine.core import FormulaError 

19from Economics.formulas.builders.native_property_formulas import ( 

20 NativePropertyExpression, 

21 generated_capital_line_property_expression, 

22 operating_line_property_expression, 

23) 

24from Economics.costing.line_properties.references import CAPITAL_LINE_KIND, OPERATING_LINE_KIND, native_property_reference 

25 

26 

27def sync_economics_line_properties_for_study(study: EconomicsStudy) -> dict[str, int]: 

28 """Materialize generated properties and formula rows for economics line items.""" 

29 

30 with transaction.atomic(): 

31 values: dict[str, int] = {} 

32 active_field_keys: set[str] = set() 

33 for line in study.capital_lines.select_related("costable_item__simulation_object", "cost_curve").order_by("pk"): 

34 field_key = _line_field_key(CAPITAL_LINE_KIND, line.pk) 

35 active_field_keys.add(field_key) 

36 values[field_key] = _sync_capital_line_property(study=study, line=line).pk 

37 for line in ( 

38 study.operating_lines.select_related( 

39 "costable_item__simulation_object", 

40 "source_property_info__set__simulationObject", 

41 "source_default_rate", 

42 ).order_by("pk") 

43 ): 

44 field_key = _line_field_key(OPERATING_LINE_KIND, line.pk) 

45 active_field_keys.add(field_key) 

46 values[field_key] = _sync_operating_line_property(study=study, line=line).pk 

47 _delete_stale_line_formulas(study, active_field_keys=active_field_keys) 

48 return values 

49 

50 

51def _sync_capital_line_property(*, study: EconomicsStudy, line: CapitalCostLine) -> PropertyValue: 

52 target = _capital_line_target(study=study, line=line) 

53 expression = _capital_line_expression(study=study, line=line) 

54 value = _materialize_line_property( 

55 study=study, 

56 property_set=target, 

57 field_key=_line_field_key(CAPITAL_LINE_KIND, line.pk), 

58 property_key="economics.capital_line", 

59 display_name=line.label, 

60 unit_type="currency", 

61 unit=line.currency or _study_currency(study), 

62 expression=expression, 

63 ) 

64 _persist_line_formula( 

65 study=study, 

66 property_value=value, 

67 line_key=_line_field_key(CAPITAL_LINE_KIND, line.pk), 

68 formula_key=f"capital_line:{line.pk}", 

69 formula=expression, 

70 capital_line=line, 

71 ) 

72 return value 

73 

74 

75def _sync_operating_line_property(*, study: EconomicsStudy, line: OperatingCostLine) -> PropertyValue: 

76 target = _operating_line_target(study=study, line=line) 

77 expression = operating_line_property_expression(line, study=study) 

78 value = _materialize_line_property( 

79 study=study, 

80 property_set=target, 

81 field_key=_line_field_key(OPERATING_LINE_KIND, line.pk), 

82 property_key="economics.operating_line", 

83 display_name=line.label, 

84 unit_type="costRate", 

85 unit=f"{line.currency or _study_currency(study)}/year", 

86 expression=expression, 

87 ) 

88 _persist_line_formula( 

89 study=study, 

90 property_value=value, 

91 line_key=_line_field_key(OPERATING_LINE_KIND, line.pk), 

92 formula_key=f"operating_line:{line.pk}", 

93 formula=expression, 

94 operating_line=line, 

95 ) 

96 return value 

97 

98 

99def _capital_line_expression(*, study: EconomicsStudy, line: CapitalCostLine) -> NativePropertyExpression: 

100 if line.source == GENERATED_CAPITAL_LINE_SOURCE: 

101 return generated_capital_line_property_expression(line) 

102 if line.amount is None and line.calculation_basis != CapitalLineBasis.BASE_CAPEX_PERCENT: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true

103 return NativePropertyExpression("", False, f"`{line.label}` has no amount.") 

104 generated_subtotal = build_generated_unit_capex_subtotal_formula(study).evaluate() 

105 if generated_subtotal is None: 

106 generated_subtotal = Decimal("0") 

107 try: 

108 formula = build_custom_capital_line_formula(line, base_capex=generated_subtotal) 

109 render_bindings = {} 

110 base_capex_reference = native_property_reference(study, "base_capital_cost") 

111 if line.calculation_basis == CapitalLineBasis.BASE_CAPEX_PERCENT and base_capex_reference: 

112 render_bindings["custom_capex_percentage_basis"] = base_capex_reference 

113 return NativePropertyExpression( 

114 formula.render_property_formula(render_bindings), 

115 True, 

116 value=formula.evaluate(), 

117 ) 

118 except FormulaError as exc: 

119 return NativePropertyExpression("", False, f"`{line.label}` {exc.message}") 

120 

121 

122def _materialize_line_property( 

123 *, 

124 study: EconomicsStudy, 

125 property_set: PropertySet, 

126 field_key: str, 

127 property_key: str, 

128 display_name: str, 

129 unit_type: str, 

130 unit: str, 

131 expression: NativePropertyExpression, 

132) -> PropertyValue: 

133 property_info = _line_property_info( 

134 study=study, 

135 property_set=property_set, 

136 field_key=field_key, 

137 property_key=property_key, 

138 display_name=display_name, 

139 unit_type=unit_type, 

140 unit=unit, 

141 ) 

142 value = _single_scalar_value(property_info) 

143 display_value = None if expression.value is None else str(expression.value) 

144 changed_fields = [] 

145 if value.value != display_value: 

146 value.value = display_value 

147 value.displayValue = display_value 

148 changed_fields.extend(["value", "displayValue"]) 

149 if value.formula != expression.formula: 

150 value.formula = expression.formula 

151 changed_fields.append("formula") 

152 if changed_fields: 

153 value.save(update_fields=changed_fields) 

154 apply_economics_property_state( 

155 property_info, 

156 editable=False, 

157 formula_incomplete=not expression.solve_visible, 

158 formula_incomplete_reason=expression.blocked_reason, 

159 ) 

160 return value 

161 

162 

163def _persist_line_formula( 

164 *, 

165 study: EconomicsStudy, 

166 property_value: PropertyValue, 

167 line_key: str, 

168 formula_key: str, 

169 formula: NativePropertyExpression, 

170 capital_line: CapitalCostLine | None = None, 

171 operating_line: OperatingCostLine | None = None, 

172) -> None: 

173 status = "calculated" if formula.value is not None and not formula.blocked_reason else "unavailable" 

174 EconomicsLineFormula.objects.update_or_create( 

175 flowsheet=study.flowsheet, 

176 study=study, 

177 line_key=line_key, 

178 defaults={ 

179 "property_value": property_value, 

180 "capital_line": capital_line, 

181 "operating_line": operating_line, 

182 "formula_key": formula_key, 

183 "formula": formula.formula, 

184 "property_formula": formula.formula, 

185 "unit": property_value.property.unit, 

186 "value": str(formula.value) if formula.value is not None else None, 

187 "status": status, 

188 "formula_audit": { 

189 "formula_key": formula_key, 

190 "formula": formula.formula, 

191 "value": str(formula.value) if formula.value is not None else None, 

192 }, 

193 "blocked_reason": formula.blocked_reason, 

194 }, 

195 ) 

196 

197 

198def _line_property_info( 

199 *, 

200 study: EconomicsStudy, 

201 property_set: PropertySet, 

202 field_key: str, 

203 property_key: str, 

204 display_name: str, 

205 unit_type: str, 

206 unit: str, 

207) -> PropertyInfo: 

208 formula_record = ( 

209 EconomicsLineFormula.objects.filter( 

210 flowsheet=study.flowsheet, 

211 study=study, 

212 line_key=field_key, 

213 property_value__isnull=False, 

214 ) 

215 .select_related("property_value__property") 

216 .order_by("pk") 

217 .first() 

218 ) 

219 property_info = formula_record.property_value.property if formula_record is not None else None 

220 defaults = { 

221 "set": property_set, 

222 "type": "numeric", 

223 "unitType": unit_type, 

224 "unit": unit, 

225 "displayName": display_name, 

226 "index": 0, 

227 } 

228 if property_info is None: 

229 return PropertyInfo.objects.create( 

230 flowsheet=study.flowsheet, 

231 key=property_key, 

232 **defaults, 

233 ) 

234 changed_fields = [] 

235 for field_name, value in defaults.items(): 

236 if getattr(property_info, field_name) != value: 236 ↛ 237line 236 didn't jump to line 237 because the condition on line 236 was never true

237 setattr(property_info, field_name, value) 

238 changed_fields.append(field_name) 

239 if property_info.key != property_key: 239 ↛ 240line 239 didn't jump to line 240 because the condition on line 239 was never true

240 property_info.key = property_key 

241 changed_fields.append("key") 

242 if changed_fields: 242 ↛ 243line 242 didn't jump to line 243 because the condition on line 242 was never true

243 property_info.save(update_fields=changed_fields) 

244 return property_info 

245 

246 

247def _single_scalar_value(property_info: PropertyInfo) -> PropertyValue: 

248 values = list(property_info.values.order_by("pk")) 

249 value = values[0] if values else None 

250 for duplicate in values[1:]: 250 ↛ 251line 250 didn't jump to line 251 because the loop on line 250 never started

251 duplicate.delete() 

252 if value is None: 

253 value = PropertyValue.objects.create( 

254 flowsheet=property_info.flowsheet, 

255 property=property_info, 

256 value=None, 

257 displayValue=None, 

258 enabled=True, 

259 ) 

260 return value 

261 

262 

263def _capital_line_target(*, study: EconomicsStudy, line: CapitalCostLine) -> PropertySet: 

264 simulation_object = None 

265 if not line.manual and line.costable_item_id: 

266 simulation_object = line.costable_item.simulation_object 

267 if simulation_object is None: 

268 return _root_property_set(study) 

269 return _property_set_for_object(study, simulation_object) 

270 

271 

272def _operating_line_target(*, study: EconomicsStudy, line: OperatingCostLine) -> PropertySet: 

273 simulation_object = None 

274 if not line.manual and line.source_property_info_id: 

275 try: 

276 simulation_object = line.source_property_info.set.simulationObject 

277 except ObjectDoesNotExist: 

278 simulation_object = None 

279 if simulation_object is None and not line.manual and line.costable_item_id: 

280 simulation_object = line.costable_item.simulation_object 

281 if simulation_object is None: 

282 return _root_property_set(study) 

283 return _property_set_for_object(study, simulation_object) 

284 

285 

286def _root_property_set(study: EconomicsStudy) -> PropertySet: 

287 root_grouping = getattr(study.flowsheet, "rootGrouping", None) 

288 try: 

289 root_object = getattr(root_grouping, "simulationObject", None) 

290 except ObjectDoesNotExist: 

291 root_object = None 

292 if root_object is not None: 

293 return _property_set_for_object(study, root_object) 

294 property_set = PropertySet.objects.filter(flowsheet=study.flowsheet, simulationObject__isnull=True).order_by("pk").first() 

295 if property_set is not None: 295 ↛ 297line 295 didn't jump to line 297 because the condition on line 295 was always true

296 return property_set 

297 return PropertySet.objects.create(flowsheet=study.flowsheet, simulationObject=None) 

298 

299 

300def _property_set_for_object(study: EconomicsStudy, simulation_object) -> PropertySet: 

301 property_set, _ = PropertySet.objects.get_or_create( 

302 flowsheet=study.flowsheet, 

303 simulationObject=simulation_object, 

304 ) 

305 return property_set 

306 

307 

308def _delete_stale_line_formulas(study: EconomicsStudy, *, active_field_keys: set[str]) -> None: 

309 stale_formulas = list( 

310 study.line_formulas.exclude(line_key__in=active_field_keys) 

311 .select_related("property_value__property") 

312 .order_by("pk") 

313 ) 

314 for formula in stale_formulas: 314 ↛ 315line 314 didn't jump to line 315 because the loop on line 314 never started

315 if formula.property_value_id and formula.property_value.property_id: 

316 formula.property_value.property.delete() 

317 study.line_formulas.exclude(line_key__in=active_field_keys).delete() 

318 

319 

320def _line_field_key(line_kind: str, line_id: int) -> str: 

321 return f"{line_kind}_line:{line_id}" 

322 

323 

324def _study_currency(study: EconomicsStudy) -> str: 

325 assumptions = get_settings_profile(study) 

326 if assumptions is None: 

327 return "NZD" 

328 return assumptions.currency or "NZD"