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

1from django.core.exceptions import ValidationError 

2from django.db import models 

3 

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 

13 

14 

15class CostIndexSeries(GlobalReferenceDataModel): 

16 """Global Stats NZ index series available to all flowsheets. 

17 

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

22 

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) 

46 

47 objects = GlobalReferenceDataManager() 

48 

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 ] 

65 

66 def __str__(self): 

67 return self.name 

68 

69 

70class CostIndexValue(GlobalReferenceDataModel): 

71 """Global period value for a Stats NZ index series.""" 

72 

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) 

94 

95 objects = GlobalReferenceDataManager() 

96 

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 ] 

105 

106 def __str__(self): 

107 return f"{self.series.key} {self.period}: {self.value}" 

108 

109 

110class EconomicsDefaultRate(GlobalReferenceDataModel): 

111 """Global source defaults and templates for v1 utility economics.""" 

112 

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) 

155 

156 objects = GlobalReferenceDataManager() 

157 

158 class Meta: 

159 ordering = ["category", "rate_type", "key"] 

160 

161 def __str__(self): 

162 return self.label 

163 

164 

165class EconomicsLangFactorDefault(GlobalReferenceDataModel): 

166 """Source-backed Lang factor defaults used to classify per-item overrides.""" 

167 

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) 

188 

189 objects = GlobalReferenceDataManager() 

190 

191 class Meta: 

192 ordering = ["scope", "unit_operation_type", "key"] 

193 

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

212 

213 def __str__(self): 

214 return self.label