Coverage for backend/django/Economics/reference_data/models.py: 86%
122 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +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 key = models.CharField(max_length=128)
24 name = models.CharField(max_length=160)
25 provider = models.CharField(max_length=128, blank=True)
26 source_series_id = models.CharField(max_length=64, blank=True)
27 frequency = models.CharField(max_length=32, blank=True)
28 unit = models.CharField(max_length=32, default="Index")
29 index_basis = models.CharField(max_length=128, blank=True)
30 source_url = models.URLField(blank=True)
31 release_title = models.CharField(max_length=160, blank=True)
32 source_asset_filename = models.CharField(max_length=160, blank=True)
33 source_asset_file_id = models.CharField(max_length=64, blank=True)
34 source_parent_id = models.CharField(max_length=64, blank=True)
35 latest_imported_period = models.CharField(max_length=16, blank=True)
36 created_at = models.DateTimeField(auto_now_add=True)
37 updated_at = models.DateTimeField(auto_now=True)
39 objects = GlobalReferenceDataManager()
41 class Meta:
42 ordering = ["provider", "key"]
43 constraints = [
44 models.UniqueConstraint(
45 fields=["key"],
46 condition=models.Q(
47 provider="Stats NZ",
48 key__in=[
49 "stats_nz_cpi_all_groups",
50 "stats_nz_cgpi_all_groups",
51 "stats_nz_pmei",
52 ],
53 ),
54 name="unique_locked_stats_nz_series_key",
55 ),
56 ]
58 def __str__(self):
59 return self.name
62class CostIndexValue(GlobalReferenceDataModel):
63 """Global period value for a Stats NZ index series."""
65 series = models.ForeignKey("CostIndexSeries", on_delete=models.CASCADE, related_name="values")
66 period = models.CharField(max_length=16)
67 period_date = models.DateField()
68 value = models.DecimalField(max_digits=20, decimal_places=8)
69 status = models.CharField(max_length=32, blank=True)
70 source_asset_filename = models.CharField(max_length=160, blank=True)
71 source_series_reference = models.CharField(max_length=64, blank=True)
72 source_period = models.CharField(max_length=16, blank=True)
73 source_units = models.CharField(max_length=32, blank=True)
74 source_subject = models.CharField(max_length=128, blank=True)
75 source_group = models.CharField(max_length=255, blank=True)
76 source_series_title_1 = models.CharField(max_length=160, blank=True)
77 created_at = models.DateTimeField(auto_now_add=True)
79 objects = GlobalReferenceDataManager()
81 class Meta:
82 ordering = ["series", "period_date"]
83 constraints = [
84 models.UniqueConstraint(
85 fields=["series", "period"],
86 name="unique_index_value_period_per_series",
87 ),
88 ]
90 def __str__(self):
91 return f"{self.series.key} {self.period}: {self.value}"
94class EconomicsDefaultRate(GlobalReferenceDataModel):
95 """Global source defaults and templates for v1 utility economics."""
97 key = models.CharField(max_length=128, unique=True)
98 label = models.CharField(max_length=160)
99 category = models.CharField(max_length=32, choices=DefaultRateCategory.choices)
100 rate_type = models.CharField(max_length=32, choices=DefaultRateType.choices)
101 value_kind = models.CharField(max_length=32, choices=DefaultRateValueKind.choices)
102 review_status = models.CharField(max_length=32, choices=DefaultRateReviewStatus.choices)
103 source_role = models.CharField(max_length=32, choices=DefaultRateSourceRole.choices)
104 value = models.DecimalField(max_digits=28, decimal_places=14, null=True, blank=True)
105 display_unit = models.CharField(max_length=64, blank=True)
106 original_value = models.DecimalField(max_digits=28, decimal_places=14, null=True, blank=True)
107 original_unit = models.CharField(max_length=64, blank=True)
108 source_url = models.URLField(blank=True)
109 source_label = models.CharField(max_length=200, blank=True)
110 source_dataset = models.CharField(max_length=200, blank=True)
111 source_table = models.CharField(max_length=200, blank=True)
112 source_release = models.CharField(max_length=200, blank=True)
113 source_year = models.CharField(max_length=64, blank=True)
114 source_asset_filename = models.CharField(max_length=200, blank=True)
115 sector = models.CharField(max_length=128, blank=True)
116 basis = models.CharField(max_length=255, blank=True)
117 nominal_real_basis = models.CharField(max_length=128, blank=True)
118 gst_treatment = models.CharField(max_length=255, blank=True)
119 conversion_formula = models.TextField(blank=True)
120 conversion_details = models.TextField(blank=True)
121 template_formula = models.TextField(blank=True)
122 range_min = models.DecimalField(max_digits=28, decimal_places=14, null=True, blank=True)
123 range_max = models.DecimalField(max_digits=28, decimal_places=14, null=True, blank=True)
124 range_unit = models.CharField(max_length=64, blank=True)
125 range_note = models.CharField(max_length=255, blank=True)
126 editable = models.BooleanField(default=True)
127 metadata = models.JSONField(default=dict, blank=True)
128 notes = models.TextField(blank=True)
129 created_at = models.DateTimeField(auto_now_add=True)
130 updated_at = models.DateTimeField(auto_now=True)
132 objects = GlobalReferenceDataManager()
134 class Meta:
135 ordering = ["category", "rate_type", "key"]
137 def __str__(self):
138 return self.label
141class EconomicsLangFactorDefault(GlobalReferenceDataModel):
142 """Source-backed Lang factor defaults used to classify per-item overrides."""
144 key = models.CharField(max_length=128, unique=True)
145 scope = models.CharField(max_length=32, choices=LangFactorDefaultScope.choices)
146 unit_operation_type = models.CharField(max_length=64, blank=True)
147 equipment_category = models.CharField(max_length=64, blank=True)
148 equipment_subtype = models.CharField(max_length=160, blank=True)
149 value = models.DecimalField(max_digits=10, decimal_places=6)
150 label = models.CharField(max_length=160)
151 source_label = models.CharField(max_length=160, blank=True)
152 review_status = models.CharField(max_length=32, choices=DefaultRateReviewStatus.choices, default=DefaultRateReviewStatus.REVIEWED)
153 notes = models.TextField(blank=True)
154 created_at = models.DateTimeField(auto_now_add=True)
155 updated_at = models.DateTimeField(auto_now=True)
157 objects = GlobalReferenceDataManager()
159 class Meta:
160 ordering = ["scope", "unit_operation_type", "key"]
162 def clean(self):
163 super().clean()
164 if self.scope == LangFactorDefaultScope.GLOBAL and self.unit_operation_type: 164 ↛ 165line 164 didn't jump to line 165 because the condition on line 164 was never true
165 raise ValidationError({"unit_operation_type": "Global Lang factor defaults must not set a unit-operation type."})
166 if self.scope == LangFactorDefaultScope.GLOBAL and (self.equipment_category or self.equipment_subtype): 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 raise ValidationError({"equipment_category": "Global Lang factor defaults must not set equipment targeting."})
168 if self.scope == LangFactorDefaultScope.UNIT_OPERATION_TYPE and not self.unit_operation_type: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true
169 raise ValidationError({"unit_operation_type": "Unit-operation Lang factor defaults require a unit-operation type."})
170 if self.scope == LangFactorDefaultScope.UNIT_OPERATION_TYPE and (self.equipment_category or self.equipment_subtype): 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true
171 raise ValidationError({"equipment_category": "Unit-operation Lang factor defaults must not set equipment targeting."})
172 if self.scope == LangFactorDefaultScope.EQUIPMENT_CATEGORY and self.unit_operation_type: 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true
173 raise ValidationError({"unit_operation_type": "Equipment Lang factor defaults must not set a unit-operation type."})
174 if self.scope == LangFactorDefaultScope.EQUIPMENT_CATEGORY and not self.equipment_category: 174 ↛ 175line 174 didn't jump to line 175 because the condition on line 174 was never true
175 raise ValidationError({"equipment_category": "Equipment Lang factor defaults require an equipment category."})
176 if self.equipment_subtype and not self.equipment_category: 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true
177 raise ValidationError({"equipment_subtype": "Equipment subtype defaults require an equipment category."})
178 if self.value <= 0: 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true
179 raise ValidationError({"value": "Lang factor defaults must be positive."})
181 def __str__(self):
182 return self.label