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

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 

7 

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 

26 

27FLOWSHEET_PARAMETER = OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT) 

28 

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} 

58 

59 

60class EconomicsSettingsProfileViewSet(ModelViewSet): 

61 """CRUD for reusable project-level economics settings profiles.""" 

62 

63 serializer_class = EconomicsSettingsProfileSerializer 

64 

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 

71 

72 def get_queryset(self): 

73 queryset = EconomicsSettingsProfile.objects.annotate( 

74 usage_count=models.Count("studies"), 

75 ) 

76 return queryset 

77 

78 @extend_schema(parameters=[FLOWSHEET_PARAMETER]) 

79 def list(self, request): 

80 return super().list(request) 

81 

82 @extend_schema(parameters=[FLOWSHEET_PARAMETER]) 

83 def retrieve(self, request, *args, **kwargs): 

84 return super().retrieve(request, *args, **kwargs) 

85 

86 @extend_schema(parameters=[FLOWSHEET_PARAMETER]) 

87 def create(self, request, *args, **kwargs): 

88 return super().create(request, *args, **kwargs) 

89 

90 @extend_schema(parameters=[FLOWSHEET_PARAMETER]) 

91 def update(self, request, *args, **kwargs): 

92 return super().update(request, *args, **kwargs) 

93 

94 @extend_schema(parameters=[FLOWSHEET_PARAMETER]) 

95 def partial_update(self, request, *args, **kwargs): 

96 return super().partial_update(request, *args, **kwargs) 

97 

98 @extend_schema(parameters=[FLOWSHEET_PARAMETER]) 

99 def destroy(self, request, *args, **kwargs): 

100 return super().destroy(request, *args, **kwargs) 

101 

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) 

106 

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") 

116 

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) 

126 

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 ) 

147 

148 

149class EconomicsAssumptionsViewSet(StudyMutationMixin, ModelViewSet): 

150 serializer_class = EconomicsAssumptionsSerializer 

151 stale_reason = "economics_assumptions_api_saved" 

152 

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 

157 

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) 

163 

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) 

169 

170 

171class EconomicsBaselineViewSet(StudyMutationMixin, ModelViewSet): 

172 serializer_class = EconomicsBaselineSerializer 

173 stale_reason = "economics_baseline_api_saved" 

174 

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 

179 

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) 

184 

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)