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

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

25 

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} 

55 

56 

57class EconomicsSettingsProfileViewSet(ModelViewSet): 

58 """CRUD for reusable flowsheet-level economics settings profiles.""" 

59 

60 serializer_class = EconomicsSettingsProfileSerializer 

61 

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) 

71 

72 @extend_schema(parameters=[OpenApiParameter(name="flowsheet", required=True, type=OpenApiTypes.INT)]) 

73 def list(self, request): 

74 return super().list(request) 

75 

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) 

79 

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

90 

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

100 

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) 

110 

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 ) 

130 

131 

132class EconomicsAssumptionsViewSet(StudyMutationMixin, ModelViewSet): 

133 serializer_class = EconomicsAssumptionsSerializer 

134 stale_reason = "economics_assumptions_api_saved" 

135 

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 

140 

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) 

146 

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) 

152 

153 

154class EconomicsBaselineViewSet(StudyMutationMixin, ModelViewSet): 

155 serializer_class = EconomicsBaselineSerializer 

156 stale_reason = "economics_baseline_api_saved" 

157 

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 

162 

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) 

167 

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)