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

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

38 

39 objects = GlobalReferenceDataManager() 

40 

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 ] 

57 

58 def __str__(self): 

59 return self.name 

60 

61 

62class CostIndexValue(GlobalReferenceDataModel): 

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

64 

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) 

78 

79 objects = GlobalReferenceDataManager() 

80 

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 ] 

89 

90 def __str__(self): 

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

92 

93 

94class EconomicsDefaultRate(GlobalReferenceDataModel): 

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

96 

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) 

131 

132 objects = GlobalReferenceDataManager() 

133 

134 class Meta: 

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

136 

137 def __str__(self): 

138 return self.label 

139 

140 

141class EconomicsLangFactorDefault(GlobalReferenceDataModel): 

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

143 

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) 

156 

157 objects = GlobalReferenceDataManager() 

158 

159 class Meta: 

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

161 

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

180 

181 def __str__(self): 

182 return self.label