Coverage for backend/django/Economics/reference_data/serializers.py: 91%

86 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-06-23 21:51 +0000

1from drf_spectacular.utils import extend_schema_field 

2from rest_framework import serializers 

3 

4from Economics.formulas.serializers import FormulaAuditSerializer 

5from Economics.reference_data.models import CostIndexSeries, EconomicsDefaultRate, EconomicsLangFactorDefault 

6from Economics.shared.choices import DefaultRateType 

7from Economics.shared.serializers import UnitOptionSerializer 

8from Economics.formulas.engine.core import FormulaError, FormulaEvaluation 

9from Economics.formulas.builders.operating import build_derived_steam_rate_formula 

10from Economics.reference_data.unit_options import default_rate_display_unit_options 

11 

12 

13class CostIndexSeriesSerializer(serializers.ModelSerializer): 

14 class Meta: 

15 model = CostIndexSeries 

16 fields = ( 

17 "id", 

18 "key", 

19 "name", 

20 "provider", 

21 "source_series_id", 

22 "frequency", 

23 "unit", 

24 "index_basis", 

25 "source_url", 

26 "release_title", 

27 "source_asset_filename", 

28 "latest_imported_period", 

29 ) 

30 

31 

32class EconomicsDefaultRateSerializer(serializers.ModelSerializer): 

33 is_numeric_default_available = serializers.SerializerMethodField() 

34 is_derived_template = serializers.SerializerMethodField() 

35 is_unavailable = serializers.SerializerMethodField() 

36 is_custom_rate = serializers.SerializerMethodField() 

37 display_unit_options = serializers.SerializerMethodField() 

38 preview_value = serializers.SerializerMethodField() 

39 formula_audit = serializers.SerializerMethodField() 

40 

41 class Meta: 

42 model = EconomicsDefaultRate 

43 fields = ( 

44 "id", 

45 "key", 

46 "label", 

47 "category", 

48 "rate_type", 

49 "value_kind", 

50 "review_status", 

51 "source_role", 

52 "value", 

53 "display_unit", 

54 "display_unit_options", 

55 "preview_value", 

56 "formula_audit", 

57 "original_value", 

58 "original_unit", 

59 "source_url", 

60 "source_label", 

61 "source_dataset", 

62 "source_table", 

63 "source_release", 

64 "source_year", 

65 "source_asset_filename", 

66 "sector", 

67 "basis", 

68 "nominal_real_basis", 

69 "gst_treatment", 

70 "conversion_formula", 

71 "conversion_details", 

72 "template_formula", 

73 "range_min", 

74 "range_max", 

75 "range_unit", 

76 "range_note", 

77 "editable", 

78 "metadata", 

79 "notes", 

80 "is_numeric_default_available", 

81 "is_derived_template", 

82 "is_unavailable", 

83 "is_custom_rate", 

84 "created_at", 

85 "updated_at", 

86 ) 

87 read_only_fields = fields 

88 

89 @extend_schema_field(serializers.BooleanField) 

90 def get_is_numeric_default_available(self, instance) -> bool: 

91 return ( 

92 instance.value is not None 

93 and instance.value_kind == "reviewed_default" 

94 and instance.review_status == "reviewed" 

95 ) 

96 

97 @extend_schema_field(serializers.BooleanField) 

98 def get_is_derived_template(self, instance) -> bool: 

99 return instance.value_kind == "derived_template" 

100 

101 @extend_schema_field(serializers.BooleanField) 

102 def get_is_unavailable(self, instance) -> bool: 

103 return instance.value_kind == "unavailable" 

104 

105 @extend_schema_field(serializers.BooleanField) 

106 def get_is_custom_rate(self, instance) -> bool: 

107 return instance.value_kind == "custom_rate" 

108 

109 @extend_schema_field(UnitOptionSerializer(many=True)) 

110 def get_display_unit_options(self, instance) -> list[dict[str, str]]: 

111 return default_rate_display_unit_options(instance) 

112 

113 @extend_schema_field(serializers.CharField(allow_null=True)) 

114 def get_preview_value(self, instance) -> str | None: 

115 return default_rate_preview_payload(instance)["value"] 

116 

117 @extend_schema_field(FormulaAuditSerializer(allow_null=True)) 

118 def get_formula_audit(self, instance) -> dict | None: 

119 return default_rate_preview_payload(instance)["formula_audit"] 

120 

121 

122class EconomicsDefaultRatePreviewRequestSerializer(serializers.Serializer): 

123 rate_type = serializers.ChoiceField(choices=DefaultRateType.choices) 

124 source_default_rate = serializers.IntegerField() 

125 boiler_efficiency_percent = serializers.DecimalField( 

126 max_digits=18, 

127 decimal_places=8, 

128 required=False, 

129 allow_null=True, 

130 ) 

131 steam_energy_gj_per_t = serializers.DecimalField( 

132 max_digits=18, 

133 decimal_places=8, 

134 required=False, 

135 allow_null=True, 

136 ) 

137 target_display_unit = serializers.CharField(required=False, allow_blank=True) 

138 

139 def validate(self, attrs): 

140 attrs = super().validate(attrs) 

141 try: 

142 default_rate = EconomicsDefaultRate.objects.get(pk=attrs["source_default_rate"]) 

143 except EconomicsDefaultRate.DoesNotExist: 

144 raise serializers.ValidationError({"source_default_rate": "Selected source default does not exist."}) from None 

145 if default_rate.rate_type != attrs["rate_type"]: 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true

146 raise serializers.ValidationError({"source_default_rate": "Selected source default does not match the rate type."}) 

147 attrs["default_rate"] = default_rate 

148 return attrs 

149 

150 

151class EconomicsDefaultRatePreviewSerializer(serializers.Serializer): 

152 rate_type = serializers.ChoiceField(choices=DefaultRateType.choices) 

153 source_default_rate = serializers.IntegerField() 

154 value = serializers.CharField(allow_null=True) 

155 unit = serializers.CharField(allow_blank=True) 

156 formula_audit = FormulaAuditSerializer(allow_null=True) 

157 warnings = serializers.ListField(child=serializers.DictField()) 

158 blocked_reason = serializers.CharField(allow_blank=True) 

159 

160 

161def default_rate_preview_payload(default_rate: EconomicsDefaultRate, *, override: dict | None = None) -> dict: 

162 """Return the public backend-evaluated preview for one default-rate source.""" 

163 if default_rate.value is not None: 

164 return { 

165 "rate_type": default_rate.rate_type, 

166 "source_default_rate": default_rate.pk, 

167 "value": str(default_rate.value), 

168 "unit": default_rate.display_unit, 

169 "formula_audit": None, 

170 "warnings": [], 

171 "blocked_reason": "", 

172 } 

173 if not default_rate.value_kind == "derived_template": 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true

174 return { 

175 "rate_type": default_rate.rate_type, 

176 "source_default_rate": default_rate.pk, 

177 "value": None, 

178 "unit": default_rate.display_unit, 

179 "formula_audit": None, 

180 "warnings": [], 

181 "blocked_reason": "", 

182 } 

183 try: 

184 formula = build_derived_steam_rate_formula(default_rate, override=override) 

185 value = formula.evaluate() 

186 except FormulaError as error: 

187 return { 

188 "rate_type": default_rate.rate_type, 

189 "source_default_rate": default_rate.pk, 

190 "value": None, 

191 "unit": default_rate.display_unit, 

192 "formula_audit": None, 

193 "warnings": [], 

194 "blocked_reason": error.message, 

195 } 

196 return { 

197 "rate_type": default_rate.rate_type, 

198 "source_default_rate": default_rate.pk, 

199 "value": str(value) if value is not None else None, 

200 "unit": default_rate.display_unit, 

201 "formula_audit": formula.formula.audit_payload(FormulaEvaluation(value=value, bindings=formula.bindings)), 

202 "warnings": [], 

203 "blocked_reason": "", 

204 } 

205 

206 

207class EconomicsLangFactorDefaultSerializer(serializers.ModelSerializer): 

208 class Meta: 

209 model = EconomicsLangFactorDefault 

210 fields = ( 

211 "id", 

212 "key", 

213 "scope", 

214 "unit_operation_type", 

215 "equipment_category", 

216 "equipment_subtype", 

217 "value", 

218 "label", 

219 "source_label", 

220 "review_status", 

221 "notes", 

222 "created_at", 

223 "updated_at", 

224 ) 

225 read_only_fields = fields