Coverage for backend/django/Economics/studies/models.py: 91%
109 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
1import uuid
3from django.core.exceptions import ValidationError
4from django.db import models
6from core.managers import AccessControlManager, AllFlowsheetStatesManager
7from Economics.shared.choices import EconomicsBaselineMode, EconomicsScheduleMode
8from Economics.shared.model_base import FlowsheetScopedEconomicsModel
11class EconomicsStudy(FlowsheetScopedEconomicsModel):
12 same_flowsheet_fields = ("schedule_scenario",)
14 flowsheet_state = models.ForeignKey("core_auxiliary.FlowsheetState", on_delete=models.CASCADE, related_name="economics_studies")
15 lineage_id = models.UUIDField(default=uuid.uuid4, editable=False)
16 settings_profile = models.ForeignKey(
17 "EconomicsSettingsProfile",
18 on_delete=models.SET_NULL,
19 related_name="studies",
20 null=True,
21 blank=True,
22 help_text="Project-scoped settings profile selected by this study.",
23 )
24 schedule_mode = models.CharField(
25 max_length=32,
26 choices=EconomicsScheduleMode.choices,
27 default=EconomicsScheduleMode.STEADY_STATE,
28 )
29 schedule_scenario = models.ForeignKey(
30 "core_auxiliary.Scenario",
31 on_delete=models.SET_NULL,
32 related_name="scheduled_economics_studies",
33 null=True,
34 blank=True,
35 help_text="MSS time-series scenario used as this study's production schedule.",
36 )
37 baseline_mode = models.CharField(
38 max_length=16,
39 choices=EconomicsBaselineMode.choices,
40 default=EconomicsBaselineMode.MANUAL,
41 help_text="Financial baseline source used when calculating baseline-dependent study metrics.",
42 )
43 baseline_flowsheet_id = models.PositiveBigIntegerField(
44 null=True,
45 blank=True,
46 help_text="Stable flowsheet identity for the selected baseline study.",
47 )
48 baseline_study_lineage_id = models.UUIDField(
49 null=True,
50 blank=True,
51 help_text="Logical study identity resolved in the target flowsheet's current state.",
52 )
53 name = models.CharField(max_length=128)
54 description = models.TextField(blank=True)
55 created_at = models.DateTimeField(auto_now_add=True)
56 updated_at = models.DateTimeField(auto_now=True)
58 objects = AccessControlManager()
59 all_states = AllFlowsheetStatesManager()
61 class Meta:
62 ordering = ["created_at"]
63 constraints = [
64 models.UniqueConstraint(
65 fields=["flowsheet_state", "name"],
66 name="unique_economics_study_name_per_state",
67 ),
68 models.UniqueConstraint(
69 fields=["flowsheet_state", "lineage_id"],
70 name="unique_economics_study_lineage_per_state",
71 ),
72 ]
74 def __str__(self):
75 return self.name
77 def clean(self):
78 super().clean()
79 if ( 79 ↛ 85line 79 didn't jump to line 85 because the condition on line 79 was never true
80 self.settings_profile_id is not None
81 and self.flowsheet_state_id is not None
82 and self.settings_profile.project_id
83 != self.flowsheet_state.flowsheet.project_id
84 ):
85 raise ValidationError(
86 {"settings_profile": "Referenced profile must belong to the study project."}
87 )
88 from Economics.studies.services.study_configuration import (
89 StudyConfigurationValidationError,
90 validate_study_configuration_for_model,
91 )
93 try:
94 validate_study_configuration_for_model(self)
95 except StudyConfigurationValidationError as exc:
96 raise ValidationError(exc.errors) from exc
98 def save(self, *args, **kwargs):
99 if (
100 self.baseline_mode == EconomicsBaselineMode.STUDY
101 or self.schedule_mode in {EconomicsScheduleMode.SCENARIO, EconomicsScheduleMode.COMPOSITE}
102 ):
103 self.clean()
104 super().save(*args, **kwargs)
107class EconomicsStudyComparisonConfig(FlowsheetScopedEconomicsModel):
108 """Saved comparison setup for one active economics study."""
110 same_flowsheet_fields = ("active_study",)
112 flowsheet_state = models.ForeignKey(
113 "core_auxiliary.FlowsheetState",
114 on_delete=models.CASCADE,
115 related_name="economics_study_comparison_configs",
116 )
117 active_study = models.OneToOneField(
118 "EconomicsStudy",
119 on_delete=models.CASCADE,
120 related_name="comparison_config",
121 )
122 baseline_mode = models.CharField(
123 max_length=16,
124 choices=EconomicsBaselineMode.choices,
125 default=EconomicsBaselineMode.STUDY,
126 help_text="Baseline source mode used for this saved comparison.",
127 )
128 baseline_flowsheet_id = models.PositiveBigIntegerField(
129 null=True,
130 blank=True,
131 help_text="Stable flowsheet identity for the selected comparison baseline.",
132 )
133 baseline_study_lineage_id = models.UUIDField(
134 null=True,
135 blank=True,
136 help_text="Logical study identity for the selected comparison baseline.",
137 )
138 created_at = models.DateTimeField(auto_now_add=True)
139 updated_at = models.DateTimeField(auto_now=True)
141 objects = AccessControlManager()
142 all_states = AllFlowsheetStatesManager()
144 class Meta:
145 ordering = ["active_study_id"]
147 def __str__(self):
148 return f"{self.active_study} comparison"
151class EconomicsStudyComparisonSelection(FlowsheetScopedEconomicsModel):
152 """Ordered saved reference to a comparison study, including display snapshots."""
154 same_flowsheet_fields = ("config",)
156 flowsheet_state = models.ForeignKey(
157 "core_auxiliary.FlowsheetState",
158 on_delete=models.CASCADE,
159 related_name="economics_study_comparison_selections",
160 )
161 config = models.ForeignKey(
162 "EconomicsStudyComparisonConfig",
163 on_delete=models.CASCADE,
164 related_name="selections",
165 )
166 target_study_lineage_id = models.UUIDField()
167 target_flowsheet_id = models.PositiveBigIntegerField()
168 study_name_snapshot = models.CharField(max_length=128)
169 flowsheet_name_snapshot = models.CharField(max_length=64)
170 sort_order = models.PositiveIntegerField(default=0)
171 created_at = models.DateTimeField(auto_now_add=True)
172 updated_at = models.DateTimeField(auto_now=True)
174 objects = AccessControlManager()
175 all_states = AllFlowsheetStatesManager()
177 class Meta:
178 ordering = ["sort_order", "pk"]
179 constraints = [
180 models.UniqueConstraint(
181 fields=["config", "target_flowsheet_id", "target_study_lineage_id"],
182 name="unique_comparison_selection_per_config_lineage",
183 ),
184 ]
186 def __str__(self):
187 return self.study_name_snapshot
190class EconomicsSchedulePlan(FlowsheetScopedEconomicsModel):
191 """Weekly composite production schedule selected by an economics study."""
193 same_flowsheet_fields = ("study",)
195 flowsheet_state = models.ForeignKey(
196 "core_auxiliary.FlowsheetState",
197 on_delete=models.CASCADE,
198 related_name="economics_schedule_plans",
199 )
200 study = models.OneToOneField(
201 "EconomicsStudy",
202 on_delete=models.CASCADE,
203 related_name="schedule_plan",
204 )
205 created_at = models.DateTimeField(auto_now_add=True)
206 updated_at = models.DateTimeField(auto_now=True)
208 objects = AccessControlManager()
209 all_states = AllFlowsheetStatesManager()
211 class Meta:
212 ordering = ["study_id"]
214 def __str__(self):
215 return f"{self.study} composite schedule"
218class EconomicsScheduleRule(FlowsheetScopedEconomicsModel):
219 """One source-scenario sequence placed at a weekly start time."""
221 same_flowsheet_fields = ("plan", "source_scenario")
223 flowsheet_state = models.ForeignKey(
224 "core_auxiliary.FlowsheetState",
225 on_delete=models.CASCADE,
226 related_name="economics_schedule_rules",
227 )
228 plan = models.ForeignKey(
229 "EconomicsSchedulePlan",
230 on_delete=models.CASCADE,
231 related_name="rules",
232 )
233 source_scenario = models.ForeignKey(
234 "core_auxiliary.Scenario",
235 on_delete=models.SET_NULL,
236 related_name="economics_schedule_rules",
237 null=True,
238 blank=True,
239 help_text="Solved MSS time-series scenario assigned to this timetable block.",
240 )
241 label = models.CharField(max_length=128, blank=True)
242 days_mask = models.PositiveSmallIntegerField(
243 help_text="Weekly day bitmask, Monday as bit 0 through Sunday as bit 6.",
244 )
245 start_time = models.TimeField()
246 sort_order = models.PositiveIntegerField(default=0)
247 created_at = models.DateTimeField(auto_now_add=True)
248 updated_at = models.DateTimeField(auto_now=True)
250 objects = AccessControlManager()
251 all_states = AllFlowsheetStatesManager()
253 class Meta:
254 ordering = ["sort_order", "pk"]
256 def __str__(self):
257 return self.label or f"{self.plan} rule {self.pk}"
259 def clean(self):
260 super().clean()
261 errors: dict[str, str] = {}
262 if not 1 <= self.days_mask <= 127: 262 ↛ 263line 262 didn't jump to line 263 because the condition on line 262 was never true
263 errors["days_mask"] = "Select at least one day for this schedule rule."
264 if errors: 264 ↛ 265line 264 didn't jump to line 265 because the condition on line 264 was never true
265 raise ValidationError(errors)