Coverage for backend/django/Economics/settings_profiles/viewsets.py: 80%
105 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 django.db import models
2from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema
3from rest_framework import status
4from rest_framework.decorators import action
5from rest_framework.exceptions import ValidationError
6from rest_framework.response import Response
8from core.viewset import ModelViewSet
9from Economics.settings_profiles.models import EconomicsAssumptions, EconomicsBaseline, EconomicsSettingsProfile
10from Economics.settings_profiles.serializers import (
11 EconomicsAssumptionsSerializer,
12 EconomicsBaselineSerializer,
13 EconomicsSettingsProfileSerializer,
14 SettingsProfileCopyRequestSerializer,
15)
16from Economics.studies.api_mutations import StudyMutationMixin
17from Economics.studies.services.result_staleness import mark_study_stale
18from Economics.shared.access import require_write_access
19from Economics.costing.operating.stream_properties import sync_project_default_operating_line_rates_for_study
20from Economics.settings_profiles.services.settings_profiles import (
21 create_settings_profile_copy,
22 update_settings_profile_from_assumptions,
23 update_settings_profile_from_baseline,
24)
25from Economics.studies.services.project_scope import current_project_from_flowsheet_context
27FLOWSHEET_PARAMETER = OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT)
29PROFILE_CALCULATION_FIELDS = {
30 "currency",
31 "location",
32 "basis_date",
33 "discount_rate_percent",
34 "project_lifetime_years",
35 "inflation_method",
36 "annual_operating_hours",
37 "tax_rate_percent",
38 "depreciation_enabled",
39 "default_depreciation_life_years",
40 "default_depreciation_salvage_percent",
41 "contingency_percent",
42 "electrical_upgrade_rate_amount",
43 "default_lang_factor",
44 "capital_index_series",
45 "operating_index_series",
46 "default_rate_overrides",
47 "manual_capex",
48 "manual_annual_opex",
49 "annual_heat_basis_mode",
50 "manual_annual_heat_basis",
51 "manual_annual_heat_basis_unit",
52 "average_power_input",
53 "average_power_unit",
54 "residual_value",
55 "notes",
56 "baseline_notes",
57}
60class EconomicsSettingsProfileViewSet(ModelViewSet):
61 """CRUD for reusable project-level economics settings profiles."""
63 serializer_class = EconomicsSettingsProfileSerializer
65 def get_serializer_context(self):
66 context = super().get_serializer_context()
67 request = context.get("request")
68 if request is not None: 68 ↛ 70line 68 didn't jump to line 70 because the condition on line 68 was always true
69 context["economics_project"] = current_project_from_flowsheet_context(request.user)
70 return context
72 def get_queryset(self):
73 queryset = EconomicsSettingsProfile.objects.annotate(
74 usage_count=models.Count("studies"),
75 )
76 return queryset
78 @extend_schema(parameters=[FLOWSHEET_PARAMETER])
79 def list(self, request):
80 return super().list(request)
82 @extend_schema(parameters=[FLOWSHEET_PARAMETER])
83 def retrieve(self, request, *args, **kwargs):
84 return super().retrieve(request, *args, **kwargs)
86 @extend_schema(parameters=[FLOWSHEET_PARAMETER])
87 def create(self, request, *args, **kwargs):
88 return super().create(request, *args, **kwargs)
90 @extend_schema(parameters=[FLOWSHEET_PARAMETER])
91 def update(self, request, *args, **kwargs):
92 return super().update(request, *args, **kwargs)
94 @extend_schema(parameters=[FLOWSHEET_PARAMETER])
95 def partial_update(self, request, *args, **kwargs):
96 return super().partial_update(request, *args, **kwargs)
98 @extend_schema(parameters=[FLOWSHEET_PARAMETER])
99 def destroy(self, request, *args, **kwargs):
100 return super().destroy(request, *args, **kwargs)
102 def perform_create(self, serializer):
103 require_write_access(self.request.user)
104 project = current_project_from_flowsheet_context(self.request.user)
105 serializer.save(project=project)
107 def perform_update(self, serializer):
108 require_write_access(self.request.user)
109 calculation_fields_changed = bool(PROFILE_CALCULATION_FIELDS.intersection(serializer.validated_data))
110 instance = serializer.save()
111 if not calculation_fields_changed:
112 return
113 for study in instance.studies.all():
114 sync_project_default_operating_line_rates_for_study(study, reason="economics_settings_profile_saved")
115 mark_study_stale(study, reason="economics_settings_profile_saved")
117 def perform_destroy(self, instance):
118 require_write_access(self.request.user)
119 if instance.studies.exists():
120 raise ValidationError(
121 {
122 "detail": "This settings profile is used by one or more studies and cannot be deleted.",
123 }
124 )
125 super().perform_destroy(instance)
127 @extend_schema(
128 parameters=[FLOWSHEET_PARAMETER],
129 request=SettingsProfileCopyRequestSerializer,
130 responses={status.HTTP_201_CREATED: EconomicsSettingsProfileSerializer},
131 )
132 @action(detail=True, methods=["post"])
133 def copy(self, request, pk=None):
134 require_write_access(request.user)
135 source = self.get_object()
136 serializer = SettingsProfileCopyRequestSerializer(data=request.data, context={"source": source})
137 serializer.is_valid(raise_exception=True)
138 profile = create_settings_profile_copy(
139 source=source,
140 name=serializer.validated_data["name"],
141 is_default=serializer.validated_data["is_default"],
142 )
143 return Response(
144 EconomicsSettingsProfileSerializer(profile, context=self.get_serializer_context()).data,
145 status=status.HTTP_201_CREATED,
146 )
149class EconomicsAssumptionsViewSet(StudyMutationMixin, ModelViewSet):
150 serializer_class = EconomicsAssumptionsSerializer
151 stale_reason = "economics_assumptions_api_saved"
153 def get_queryset(self):
154 queryset = EconomicsAssumptions.objects.select_related("study")
155 study = self.request.query_params.get("study")
156 return queryset.filter(study_id=study) if study is not None else queryset
158 def perform_create(self, serializer):
159 instance = serializer.save()
160 update_settings_profile_from_assumptions(instance)
161 sync_project_default_operating_line_rates_for_study(instance.study, reason=self.stale_reason)
162 mark_study_stale(instance.study, reason=self.stale_reason)
164 def perform_update(self, serializer):
165 instance = serializer.save()
166 update_settings_profile_from_assumptions(instance)
167 sync_project_default_operating_line_rates_for_study(instance.study, reason=self.stale_reason)
168 mark_study_stale(instance.study, reason=self.stale_reason)
171class EconomicsBaselineViewSet(StudyMutationMixin, ModelViewSet):
172 serializer_class = EconomicsBaselineSerializer
173 stale_reason = "economics_baseline_api_saved"
175 def get_queryset(self):
176 queryset = EconomicsBaseline.objects.select_related("study")
177 study = self.request.query_params.get("study")
178 return queryset.filter(study_id=study) if study is not None else queryset
180 def perform_create(self, serializer):
181 instance = serializer.save()
182 update_settings_profile_from_baseline(instance)
183 mark_study_stale(instance.study, reason=self.stale_reason)
185 def perform_update(self, serializer):
186 instance = serializer.save()
187 update_settings_profile_from_baseline(instance)
188 mark_study_stale(instance.study, reason=self.stale_reason)