Coverage for backend/django/Economics/studies/services/study_copy.py: 63%

63 statements  

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

1from django.core.exceptions import ObjectDoesNotExist 

2from django.db import transaction 

3from rest_framework.exceptions import ValidationError 

4 

5from Economics.costing.models import CapitalCostLine, CostDriver, CostableItem, EquipmentMapping, OperatingCostLine 

6from Economics.settings_profiles.models import EconomicsAssumptions, EconomicsBaseline 

7from Economics.studies.models import EconomicsStudy 

8 

9 

10def duplicate_study(source: EconomicsStudy, *, requested_name: str) -> EconomicsStudy: 

11 """Copy editable study configuration without copying result runs or active-study state.""" 

12 with transaction.atomic(): 

13 study = EconomicsStudy.objects.create( 

14 flowsheet=source.flowsheet, 

15 settings_profile=source.settings_profile, 

16 name=_duplicate_name(source, requested_name=requested_name), 

17 description=source.description, 

18 ) 

19 costable_item_map = _copy_costable_items(source=source, target=study) 

20 _copy_capital_lines(source=source, target=study, costable_item_map=costable_item_map) 

21 _copy_operating_lines(source=source, target=study, costable_item_map=costable_item_map) 

22 return study 

23 

24 

25def _duplicate_name(source: EconomicsStudy, *, requested_name: str) -> str: 

26 if requested_name: 26 ↛ 31line 26 didn't jump to line 31 because the condition on line 26 was always true

27 if EconomicsStudy.objects.filter(flowsheet=source.flowsheet, name=requested_name).exists(): 27 ↛ 28line 27 didn't jump to line 28 because the condition on line 27 was never true

28 raise ValidationError({"name": "An economics study with this name already exists in the flowsheet."}) 

29 return requested_name 

30 

31 base_name = f"{source.name} Copy" 

32 candidate = base_name 

33 suffix = 2 

34 while EconomicsStudy.objects.filter(flowsheet=source.flowsheet, name=candidate).exists(): 

35 candidate = f"{base_name} {suffix}" 

36 suffix += 1 

37 return candidate 

38 

39 

40def _copy_assumptions(*, source: EconomicsStudy, target: EconomicsStudy) -> None: 

41 try: 

42 assumptions = source.assumptions 

43 except ObjectDoesNotExist: 

44 return 

45 EconomicsAssumptions.objects.create( 

46 flowsheet=target.flowsheet, 

47 study=target, 

48 currency=assumptions.currency, 

49 location=assumptions.location, 

50 basis_date=assumptions.basis_date, 

51 discount_rate_percent=assumptions.discount_rate_percent, 

52 project_lifetime_years=assumptions.project_lifetime_years, 

53 inflation_method=assumptions.inflation_method, 

54 annual_operating_hours=assumptions.annual_operating_hours, 

55 tax_rate_percent=assumptions.tax_rate_percent, 

56 depreciation_enabled=assumptions.depreciation_enabled, 

57 default_depreciation_life_years=assumptions.default_depreciation_life_years, 

58 default_depreciation_salvage_percent=assumptions.default_depreciation_salvage_percent, 

59 contingency_percent=assumptions.contingency_percent, 

60 electrical_upgrade_rate_amount=assumptions.electrical_upgrade_rate_amount, 

61 default_lang_factor=assumptions.default_lang_factor, 

62 capital_index_series=assumptions.capital_index_series, 

63 operating_index_series=assumptions.operating_index_series, 

64 default_rate_overrides=assumptions.default_rate_overrides, 

65 notes=assumptions.notes, 

66 ) 

67 

68 

69def _copy_baseline(*, source: EconomicsStudy, target: EconomicsStudy) -> None: 

70 try: 

71 baseline = source.baseline 

72 except ObjectDoesNotExist: 

73 return 

74 EconomicsBaseline.objects.create( 

75 flowsheet=target.flowsheet, 

76 study=target, 

77 manual_capex=baseline.manual_capex, 

78 manual_annual_opex=baseline.manual_annual_opex, 

79 annual_heat_basis_mode=baseline.annual_heat_basis_mode, 

80 manual_annual_heat_basis=baseline.manual_annual_heat_basis, 

81 manual_annual_heat_basis_unit=baseline.manual_annual_heat_basis_unit, 

82 average_power_input=baseline.average_power_input, 

83 average_power_unit=baseline.average_power_unit, 

84 residual_value=baseline.residual_value, 

85 notes=baseline.notes, 

86 ) 

87 

88 

89def _copy_costable_items(*, source: EconomicsStudy, target: EconomicsStudy) -> dict[int, CostableItem]: 

90 costable_item_map = {} 

91 for item in source.costable_items.select_related("simulation_object").order_by("created_at", "pk"): 

92 copied = CostableItem.objects.create( 

93 flowsheet=target.flowsheet, 

94 study=target, 

95 item_type=item.item_type, 

96 simulation_object=item.simulation_object, 

97 name=item.name, 

98 included=item.included, 

99 manual=item.manual, 

100 notes=item.notes, 

101 ) 

102 costable_item_map[item.pk] = copied 

103 _copy_cost_driver(source_item=item, target_item=copied) 

104 _copy_equipment_mapping(source_item=item, target_item=copied) 

105 return costable_item_map 

106 

107 

108def _copy_cost_driver(*, source_item: CostableItem, target_item: CostableItem) -> None: 

109 try: 

110 driver = source_item.cost_driver 

111 except ObjectDoesNotExist: 

112 return 

113 CostDriver.objects.create( 

114 flowsheet=target_item.flowsheet, 

115 costable_item=target_item, 

116 source=driver.source, 

117 property_info=driver.property_info, 

118 manual_property_info=driver.manual_property_info, 

119 sizing_mode=driver.sizing_mode, 

120 canonical_unit=driver.canonical_unit, 

121 design_value=driver.design_value, 

122 unresolved_reason_code=driver.unresolved_reason_code, 

123 warning_payload=driver.warning_payload, 

124 ) 

125 

126 

127def _copy_equipment_mapping(*, source_item: CostableItem, target_item: CostableItem) -> None: 

128 try: 

129 mapping = source_item.equipment_mapping 

130 except ObjectDoesNotExist: 

131 return 

132 EquipmentMapping.objects.create( 

133 flowsheet=target_item.flowsheet, 

134 costable_item=target_item, 

135 cost_curve=mapping.cost_curve, 

136 equipment_category=mapping.equipment_category, 

137 equipment_subtype=mapping.equipment_subtype, 

138 cost_basis=mapping.cost_basis, 

139 install_factor_profile=mapping.install_factor_profile, 

140 install_factor=mapping.install_factor, 

141 use_study_lang_factor=mapping.use_study_lang_factor, 

142 applicability_notes=mapping.applicability_notes, 

143 ) 

144 

145 

146def _copy_capital_lines(*, source: EconomicsStudy, target: EconomicsStudy, costable_item_map: dict[int, CostableItem]) -> None: 

147 for line in source.capital_lines.order_by("created_at", "pk"): 

148 CapitalCostLine.objects.create( 

149 flowsheet=target.flowsheet, 

150 study=target, 

151 costable_item=costable_item_map.get(line.costable_item_id), 

152 cost_curve=line.cost_curve, 

153 label=line.label, 

154 line_type=line.line_type, 

155 calculation_basis=line.calculation_basis, 

156 amount=line.amount, 

157 basis_percent=line.basis_percent, 

158 depreciation_mode=line.depreciation_mode, 

159 depreciation_life_years=line.depreciation_life_years, 

160 depreciation_salvage_percent=line.depreciation_salvage_percent, 

161 peak_demand_kw=line.peak_demand_kw, 

162 minimum_peak_demand_kw=line.minimum_peak_demand_kw, 

163 currency=line.currency, 

164 included=line.included, 

165 manual=line.manual, 

166 source=line.source, 

167 confidence=line.confidence, 

168 warning_payload=line.warning_payload, 

169 driver_inputs=line.driver_inputs, 

170 ) 

171 

172 

173def _copy_operating_lines(*, source: EconomicsStudy, target: EconomicsStudy, costable_item_map: dict[int, CostableItem]) -> None: 

174 for line in source.operating_lines.order_by("created_at", "pk"): 174 ↛ 175line 174 didn't jump to line 175 because the loop on line 174 never started

175 OperatingCostLine.objects.create( 

176 flowsheet=target.flowsheet, 

177 study=target, 

178 costable_item=costable_item_map.get(line.costable_item_id), 

179 label=line.label, 

180 line_type=line.line_type, 

181 category=line.category, 

182 economic_effect=line.economic_effect, 

183 currency=line.currency, 

184 basis_quantity=line.basis_quantity, 

185 basis_unit=line.basis_unit, 

186 basis_quantity_source=line.basis_quantity_source, 

187 rate_amount=line.rate_amount, 

188 rate_unit=line.rate_unit, 

189 rate_type=line.rate_type, 

190 rate_source_mode=line.rate_source_mode, 

191 calculation_method=line.calculation_method, 

192 source_property_info=line.source_property_info, 

193 source_default_rate=line.source_default_rate, 

194 outlet_stream_disposition=line.outlet_stream_disposition, 

195 included=line.included, 

196 manual=line.manual, 

197 source=line.source, 

198 warning_payload=line.warning_payload, 

199 )