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
« 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
5from Economics.costing.models import CapitalCostLine, CostDriver, CostableItem, EquipmentMapping, OperatingCostLine
6from Economics.settings_profiles.models import EconomicsAssumptions, EconomicsBaseline
7from Economics.studies.models import EconomicsStudy
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
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
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
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 )
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 )
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
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 )
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 )
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 )
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 )