Coverage for backend/django/Economics/settings_profiles/viewsets.py: 74%
92 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.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.validation import get_current_flowsheet
9from core.viewset import ModelViewSet
10from Economics.settings_profiles.models import EconomicsAssumptions, EconomicsBaseline, EconomicsSettingsProfile
11from Economics.settings_profiles.serializers import (
12 EconomicsAssumptionsSerializer,
13 EconomicsBaselineSerializer,
14 EconomicsSettingsProfileSerializer,
15 SettingsProfileCopyRequestSerializer,
16)
17from Economics.studies.api_mutations import StudyMutationMixin, _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)
26PROFILE_CALCULATION_FIELDS = {
27 "currency",
28 "location",
29 "basis_date",
30 "discount_rate_percent",
31 "project_lifetime_years",
32 "inflation_method",
33 "annual_operating_hours",
34 "tax_rate_percent",
35 "depreciation_enabled",
36 "default_depreciation_life_years",
37 "default_depreciation_salvage_percent",
38 "contingency_percent",
39 "electrical_upgrade_rate_amount",
40 "default_lang_factor",
41 "capital_index_series",
42 "operating_index_series",
43 "default_rate_overrides",
44 "manual_capex",
45 "manual_annual_opex",
46 "annual_heat_basis_mode",
47 "manual_annual_heat_basis",
48 "manual_annual_heat_basis_unit",
49 "average_power_input",
50 "average_power_unit",
51 "residual_value",
52 "notes",
53 "baseline_notes",
54}
57class EconomicsSettingsProfileViewSet(ModelViewSet):
58 """CRUD for reusable flowsheet-level economics settings profiles."""
60 serializer_class = EconomicsSettingsProfileSerializer
62 def get_queryset(self):
63 queryset = EconomicsSettingsProfile.objects.annotate(
64 usage_count=models.Count("studies"),
65 )
66 context = get_current_flowsheet() or {}
67 flowsheet = self.request.query_params.get("flowsheet") or context.get("flowsheet")
68 if flowsheet is None:
69 return queryset.none()
70 return queryset.filter(flowsheet_id=flowsheet)
72 @extend_schema(parameters=[OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT)])
73 def list(self, request):
74 return super().list(request)
76 @extend_schema(parameters=[OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT)])
77 def create(self, request, *args, **kwargs):
78 return super().create(request, *args, **kwargs)
80 def perform_create(self, serializer):
81 _require_write_access(self.request.user)
82 context = get_current_flowsheet() or {}
83 flowsheet_id = context.get("flowsheet")
84 if serializer.validated_data.get("is_default") and flowsheet_id is not None:
85 EconomicsSettingsProfile.objects.filter(
86 flowsheet_id=flowsheet_id,
87 is_default=True,
88 ).update(is_default=False)
89 serializer.save()
91 def perform_update(self, serializer):
92 _require_write_access(self.request.user)
93 calculation_fields_changed = bool(PROFILE_CALCULATION_FIELDS.intersection(serializer.validated_data))
94 instance = serializer.save()
95 if not calculation_fields_changed:
96 return
97 for study in instance.studies.all():
98 sync_project_default_operating_line_rates_for_study(study, reason="economics_settings_profile_saved")
99 _mark_study_stale(study, reason="economics_settings_profile_saved")
101 def perform_destroy(self, instance):
102 _require_write_access(self.request.user)
103 if instance.studies.exists():
104 raise ValidationError(
105 {
106 "detail": "This settings profile is used by one or more studies and cannot be deleted.",
107 }
108 )
109 super().perform_destroy(instance)
111 @extend_schema(
112 request=SettingsProfileCopyRequestSerializer,
113 responses={status.HTTP_201_CREATED: EconomicsSettingsProfileSerializer},
114 )
115 @action(detail=True, methods=["post"])
116 def copy(self, request, pk=None):
117 _require_write_access(request.user)
118 source = self.get_object()
119 serializer = SettingsProfileCopyRequestSerializer(data=request.data, context={"source": source})
120 serializer.is_valid(raise_exception=True)
121 profile = create_settings_profile_copy(
122 source=source,
123 name=serializer.validated_data["name"],
124 is_default=serializer.validated_data["is_default"],
125 )
126 return Response(
127 EconomicsSettingsProfileSerializer(profile, context=self.get_serializer_context()).data,
128 status=status.HTTP_201_CREATED,
129 )
132class EconomicsAssumptionsViewSet(StudyMutationMixin, ModelViewSet):
133 serializer_class = EconomicsAssumptionsSerializer
134 stale_reason = "economics_assumptions_api_saved"
136 def get_queryset(self):
137 queryset = EconomicsAssumptions.objects.select_related("study")
138 study = self.request.query_params.get("study")
139 return queryset.filter(study_id=study) if study is not None else queryset
141 def perform_create(self, serializer):
142 instance = serializer.save()
143 update_settings_profile_from_assumptions(instance)
144 sync_project_default_operating_line_rates_for_study(instance.study, reason=self.stale_reason)
145 _mark_study_stale(instance.study, reason=self.stale_reason)
147 def perform_update(self, serializer):
148 instance = serializer.save()
149 update_settings_profile_from_assumptions(instance)
150 sync_project_default_operating_line_rates_for_study(instance.study, reason=self.stale_reason)
151 _mark_study_stale(instance.study, reason=self.stale_reason)
154class EconomicsBaselineViewSet(StudyMutationMixin, ModelViewSet):
155 serializer_class = EconomicsBaselineSerializer
156 stale_reason = "economics_baseline_api_saved"
158 def get_queryset(self):
159 queryset = EconomicsBaseline.objects.select_related("study")
160 study = self.request.query_params.get("study")
161 return queryset.filter(study_id=study) if study is not None else queryset
163 def perform_create(self, serializer):
164 instance = serializer.save()
165 update_settings_profile_from_baseline(instance)
166 _mark_study_stale(instance.study, reason=self.stale_reason)
168 def perform_update(self, serializer):
169 instance = serializer.save()
170 update_settings_profile_from_baseline(instance)
171 _mark_study_stale(instance.study, reason=self.stale_reason)