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
« 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
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
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 )
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()
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
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 )
97 @extend_schema_field(serializers.BooleanField)
98 def get_is_derived_template(self, instance) -> bool:
99 return instance.value_kind == "derived_template"
101 @extend_schema_field(serializers.BooleanField)
102 def get_is_unavailable(self, instance) -> bool:
103 return instance.value_kind == "unavailable"
105 @extend_schema_field(serializers.BooleanField)
106 def get_is_custom_rate(self, instance) -> bool:
107 return instance.value_kind == "custom_rate"
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)
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"]
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"]
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)
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
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)
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 }
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