Coverage for backend/django/Economics/reference_data/models.py: 87%
126 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.core.exceptions import ValidationError
2from django.db import models
4from Economics.shared.choices import (
5 DefaultRateCategory,
6 DefaultRateReviewStatus,
7 DefaultRateSourceRole,
8 DefaultRateType,
9 DefaultRateValueKind,
10 LangFactorDefaultScope,
11)
12from Economics.shared.model_base import GlobalReferenceDataManager, GlobalReferenceDataModel
15class CostIndexSeries(GlobalReferenceDataModel):
16 """Global Stats NZ index series available to all flowsheets.
18 Index source data is public/reference data, not user-authored flowsheet
19 configuration. Economics studies reference these rows, but access-control
20 scoping remains on the study, assumptions, result, and line models.
21 """
23 flowsheet = models.ForeignKey(
24 "core_auxiliary.Flowsheet",
25 on_delete=models.CASCADE,
26 related_name="economics_index_series",
27 null=True,
28 blank=True,
29 help_text="Deprecated compatibility field; Stats NZ economics index rows are global and use NULL.",
30 )
31 key = models.CharField(max_length=128)
32 name = models.CharField(max_length=160)
33 provider = models.CharField(max_length=128, blank=True)
34 source_series_id = models.CharField(max_length=64, blank=True)
35 frequency = models.CharField(max_length=32, blank=True)
36 unit = models.CharField(max_length=32, default="Index")
37 index_basis = models.CharField(max_length=128, blank=True)
38 source_url = models.URLField(blank=True)
39 release_title = models.CharField(max_length=160, blank=True)
40 source_asset_filename = models.CharField(max_length=160, blank=True)
41 source_asset_file_id = models.CharField(max_length=64, blank=True)
42 source_parent_id = models.CharField(max_length=64, blank=True)
43 latest_imported_period = models.CharField(max_length=16, blank=True)
44 created_at = models.DateTimeField(auto_now_add=True)
45 updated_at = models.DateTimeField(auto_now=True)
47 objects = GlobalReferenceDataManager()
49 class Meta:
50 ordering = ["provider", "key"]
51 constraints = [
52 models.UniqueConstraint(
53 fields=["key"],
54 condition=models.Q(
55 provider="Stats NZ",
56 key__in=[
57 "stats_nz_cpi_all_groups",
58 "stats_nz_cgpi_all_groups",
59 "stats_nz_pmei",
60 ],
61 ),
62 name="unique_locked_stats_nz_series_key",
63 ),
64 ]
66 def __str__(self):
67 return self.name
70class CostIndexValue(GlobalReferenceDataModel):
71 """Global period value for a Stats NZ index series."""
73 flowsheet = models.ForeignKey(
74 "core_auxiliary.Flowsheet",
75 on_delete=models.CASCADE,
76 related_name="economics_index_values",
77 null=True,
78 blank=True,
79 help_text="Deprecated compatibility field; Stats NZ economics index rows are global and use NULL.",
80 )
81 series = models.ForeignKey("CostIndexSeries", on_delete=models.CASCADE, related_name="values")
82 period = models.CharField(max_length=16)
83 period_date = models.DateField()
84 value = models.DecimalField(max_digits=20, decimal_places=8)
85 status = models.CharField(max_length=32, blank=True)
86 source_asset_filename = models.CharField(max_length=160, blank=True)
87 source_series_reference = models.CharField(max_length=64, blank=True)
88 source_period = models.CharField(max_length=16, blank=True)
89 source_units = models.CharField(max_length=32, blank=True)
90 source_subject = models.CharField(max_length=128, blank=True)
91 source_group = models.CharField(max_length=255, blank=True)
92 source_series_title_1 = models.CharField(max_length=160, blank=True)
93 created_at = models.DateTimeField(auto_now_add=True)
95 objects = GlobalReferenceDataManager()
97 class Meta:
98 ordering = ["series", "period_date"]
99 constraints = [
100 models.UniqueConstraint(
101 fields=["series", "period"],
102 name="unique_index_value_period_per_series",
103 ),
104 ]
106 def __str__(self):
107 return f"{self.series.key} {self.period}: {self.value}"
110class EconomicsDefaultRate(GlobalReferenceDataModel):
111 """Global source defaults and templates for v1 utility economics."""
113 flowsheet = models.ForeignKey(
114 "core_auxiliary.Flowsheet",
115 on_delete=models.CASCADE,
116 related_name="economics_default_rates",
117 null=True,
118 blank=True,
119 help_text="Deprecated compatibility field; economics default rates are global and use NULL.",
120 )
121 key = models.CharField(max_length=128, unique=True)
122 label = models.CharField(max_length=160)
123 category = models.CharField(max_length=32, choices=DefaultRateCategory.choices)
124 rate_type = models.CharField(max_length=32, choices=DefaultRateType.choices)
125 value_kind = models.CharField(max_length=32, choices=DefaultRateValueKind.choices)
126 review_status = models.CharField(max_length=32, choices=DefaultRateReviewStatus.choices)
127 source_role = models.CharField(max_length=32, choices=DefaultRateSourceRole.choices)
128 value = models.DecimalField(max_digits=28, decimal_places=14, null=True, blank=True)
129 display_unit = models.CharField(max_length=64, blank=True)
130 original_value = models.DecimalField(max_digits=28, decimal_places=14, null=True, blank=True)
131 original_unit = models.CharField(max_length=64, blank=True)
132 source_url = models.URLField(blank=True)
133 source_label = models.CharField(max_length=200, blank=True)
134 source_dataset = models.CharField(max_length=200, blank=True)
135 source_table = models.CharField(max_length=200, blank=True)
136 source_release = models.CharField(max_length=200, blank=True)
137 source_year = models.CharField(max_length=64, blank=True)
138 source_asset_filename = models.CharField(max_length=200, blank=True)
139 sector = models.CharField(max_length=128, blank=True)
140 basis = models.CharField(max_length=255, blank=True)
141 nominal_real_basis = models.CharField(max_length=128, blank=True)
142 gst_treatment = models.CharField(max_length=255, blank=True)
143 conversion_formula = models.TextField(blank=True)
144 conversion_details = models.TextField(blank=True)
145 template_formula = models.TextField(blank=True)
146 range_min = models.DecimalField(max_digits=28, decimal_places=14, null=True, blank=True)
147 range_max = models.DecimalField(max_digits=28, decimal_places=14, null=True, blank=True)
148 range_unit = models.CharField(max_length=64, blank=True)
149 range_note = models.CharField(max_length=255, blank=True)
150 editable = models.BooleanField(default=True)
151 metadata = models.JSONField(default=dict, blank=True)
152 notes = models.TextField(blank=True)
153 created_at = models.DateTimeField(auto_now_add=True)
154 updated_at = models.DateTimeField(auto_now=True)
156 objects = GlobalReferenceDataManager()
158 class Meta:
159 ordering = ["category", "rate_type", "key"]
161 def __str__(self):
162 return self.label
165class EconomicsLangFactorDefault(GlobalReferenceDataModel):
166 """Source-backed Lang factor defaults used to classify per-item overrides."""
168 flowsheet = models.ForeignKey(
169 "core_auxiliary.Flowsheet",
170 on_delete=models.CASCADE,
171 related_name="economics_lang_factor_defaults",
172 null=True,
173 blank=True,
174 help_text="Compatibility field; v1 Lang factor defaults are global reference data and use NULL.",
175 )
176 key = models.CharField(max_length=128, unique=True)
177 scope = models.CharField(max_length=32, choices=LangFactorDefaultScope.choices)
178 unit_operation_type = models.CharField(max_length=64, blank=True)
179 equipment_category = models.CharField(max_length=64, blank=True)
180 equipment_subtype = models.CharField(max_length=160, blank=True)
181 value = models.DecimalField(max_digits=10, decimal_places=6)
182 label = models.CharField(max_length=160)
183 source_label = models.CharField(max_length=160, blank=True)
184 review_status = models.CharField(max_length=32, choices=DefaultRateReviewStatus.choices, default=DefaultRateReviewStatus.REVIEWED)
185 notes = models.TextField(blank=True)
186 created_at = models.DateTimeField(auto_now_add=True)
187 updated_at = models.DateTimeField(auto_now=True)
189 objects = GlobalReferenceDataManager()
191 class Meta:
192 ordering = ["scope", "unit_operation_type", "key"]
194 def clean(self):
195 super().clean()
196 if self.scope == LangFactorDefaultScope.GLOBAL and self.unit_operation_type: 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true
197 raise ValidationError({"unit_operation_type": "Global Lang factor defaults must not set a unit-operation type."})
198 if self.scope == LangFactorDefaultScope.GLOBAL and (self.equipment_category or self.equipment_subtype): 198 ↛ 199line 198 didn't jump to line 199 because the condition on line 198 was never true
199 raise ValidationError({"equipment_category": "Global Lang factor defaults must not set equipment targeting."})
200 if self.scope == LangFactorDefaultScope.UNIT_OPERATION_TYPE and not self.unit_operation_type: 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true
201 raise ValidationError({"unit_operation_type": "Unit-operation Lang factor defaults require a unit-operation type."})
202 if self.scope == LangFactorDefaultScope.UNIT_OPERATION_TYPE and (self.equipment_category or self.equipment_subtype): 202 ↛ 203line 202 didn't jump to line 203 because the condition on line 202 was never true
203 raise ValidationError({"equipment_category": "Unit-operation Lang factor defaults must not set equipment targeting."})
204 if self.scope == LangFactorDefaultScope.EQUIPMENT_CATEGORY and self.unit_operation_type: 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true
205 raise ValidationError({"unit_operation_type": "Equipment Lang factor defaults must not set a unit-operation type."})
206 if self.scope == LangFactorDefaultScope.EQUIPMENT_CATEGORY and not self.equipment_category: 206 ↛ 207line 206 didn't jump to line 207 because the condition on line 206 was never true
207 raise ValidationError({"equipment_category": "Equipment Lang factor defaults require an equipment category."})
208 if self.equipment_subtype and not self.equipment_category: 208 ↛ 209line 208 didn't jump to line 209 because the condition on line 208 was never true
209 raise ValidationError({"equipment_subtype": "Equipment subtype defaults require an equipment category."})
210 if self.value <= 0: 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true
211 raise ValidationError({"value": "Lang factor defaults must be positive."})
213 def __str__(self):
214 return self.label