642 lines
25 KiB
Python
642 lines
25 KiB
Python
"""
|
|
Quality app forms with healthcare-focused validation and user experience.
|
|
"""
|
|
|
|
from django import forms
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
from datetime import date, timedelta
|
|
|
|
from accounts.models import User
|
|
from .models import (
|
|
QualityIndicator, QualityMeasurement, IncidentReport, RiskAssessment,
|
|
AuditPlan, AuditFinding, ImprovementProject
|
|
)
|
|
|
|
|
|
class QualityIndicatorForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating quality indicators.
|
|
"""
|
|
|
|
class Meta:
|
|
model = QualityIndicator
|
|
fields = [
|
|
'name', 'description', 'category', 'measurement_unit',
|
|
'target_value', 'threshold_warning', 'threshold_critical',
|
|
'frequency', 'data_source', 'calculation_method',
|
|
'is_active',
|
|
]
|
|
widgets = {
|
|
'name': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter indicator name'
|
|
}),
|
|
'description': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Describe what this indicator measures'
|
|
}),
|
|
'category': forms.Select(attrs={'class': 'form-control'}),
|
|
'measurement_unit': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'e.g., %, count, ratio'
|
|
}),
|
|
'target_value': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'step': '0.01',
|
|
'min': '0'
|
|
}),
|
|
'threshold_warning': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'step': '0.01',
|
|
'min': '0'
|
|
}),
|
|
'threshold_critical': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'step': '0.01',
|
|
'min': '0'
|
|
}),
|
|
'frequency': forms.Select(attrs={'class': 'form-control'}),
|
|
'data_source': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Source of measurement data'
|
|
}),
|
|
'calculation_method': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'How is this indicator calculated?'
|
|
}),
|
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
# 'notes': forms.Textarea(attrs={
|
|
# 'class': 'form-control',
|
|
# 'rows': 2,
|
|
# 'placeholder': 'Additional notes'
|
|
# }),
|
|
}
|
|
help_texts = {
|
|
'target_value': 'The desired value for this indicator',
|
|
'threshold_warning': 'Value that triggers a warning alert',
|
|
'threshold_critical': 'Value that triggers a critical alert',
|
|
'measurement_frequency': 'How often should this indicator be measured?',
|
|
}
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
target_value = cleaned_data.get('target_value')
|
|
threshold_warning = cleaned_data.get('threshold_warning')
|
|
threshold_critical = cleaned_data.get('threshold_critical')
|
|
|
|
# Validate threshold relationships
|
|
if all([target_value, threshold_warning, threshold_critical]):
|
|
if threshold_critical >= threshold_warning:
|
|
raise ValidationError(
|
|
'Critical threshold must be lower than warning threshold for most indicators.'
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class QualityMeasurementForm(forms.ModelForm):
|
|
"""
|
|
Form for creating quality measurements.
|
|
"""
|
|
|
|
class Meta:
|
|
model = QualityMeasurement
|
|
fields = [
|
|
'indicator', 'measurement_date', 'value', 'data_source',
|
|
'measurement_method', 'sample_size', 'notes'
|
|
]
|
|
widgets = {
|
|
'indicator': forms.Select(attrs={'class': 'form-control'}),
|
|
'measurement_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
'value': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'step': '0.01'
|
|
}),
|
|
'data_source': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Source of measurement data'
|
|
}),
|
|
'measurement_method': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'How was this measurement obtained?'
|
|
}),
|
|
'sample_size': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'min': '1'
|
|
}),
|
|
'notes': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'Additional notes about this measurement'
|
|
}),
|
|
}
|
|
help_texts = {
|
|
'measurement_date': 'Date when the measurement was taken',
|
|
'value': 'The measured value for this indicator',
|
|
'sample_size': 'Number of data points used in this measurement',
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
# Filter indicators by tenant and active status
|
|
self.fields['indicator'].queryset = QualityIndicator.objects.filter(
|
|
tenant=user.tenant,
|
|
is_active=True
|
|
).order_by('category', 'indicator_name')
|
|
|
|
def clean_measurement_date(self):
|
|
measurement_date = self.cleaned_data.get('measurement_date')
|
|
if measurement_date and measurement_date > timezone.now().date():
|
|
raise ValidationError('Measurement date cannot be in the future.')
|
|
return measurement_date
|
|
|
|
def clean_value(self):
|
|
value = self.cleaned_data.get('value')
|
|
if value is not None and value < 0:
|
|
raise ValidationError('Measurement value cannot be negative.')
|
|
return value
|
|
|
|
|
|
class IncidentReportForm(forms.ModelForm):
|
|
"""
|
|
Form for creating incident reports.
|
|
"""
|
|
|
|
class Meta:
|
|
model = IncidentReport
|
|
fields = [
|
|
'title', 'description', 'incident_type', 'severity',
|
|
'incident_date', 'incident_time', 'location',
|
|
'corrective_actions', 'contributing_factors',
|
|
'patient', 'witness_information'
|
|
]
|
|
widgets = {
|
|
'title': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Brief title describing the incident'
|
|
}),
|
|
'description': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 4,
|
|
'placeholder': 'Detailed description of what happened'
|
|
}),
|
|
'incident_type': forms.Select(attrs={'class': 'form-control'}),
|
|
'severity': forms.Select(attrs={'class': 'form-control'}),
|
|
'incident_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
'incident_time': forms.TimeInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'time'
|
|
}),
|
|
'location': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Where did the incident occur?'
|
|
}),
|
|
'department': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Department involved'
|
|
}),
|
|
# 'persons_involved': forms.Textarea(attrs={
|
|
# 'class': 'form-control',
|
|
# 'rows': 2,
|
|
# 'placeholder': 'Names and roles of persons involved'
|
|
# }),
|
|
'corrective_actions': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'What actions were taken immediately?'
|
|
}),
|
|
'contributing_factors': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'What factors contributed to this incident?'
|
|
}),
|
|
'patient': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'witness_information': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'Witness names and contact information'
|
|
}),
|
|
}
|
|
help_texts = {
|
|
'severity': 'Select the severity level based on actual or potential impact',
|
|
'incident_date': 'Date when the incident occurred',
|
|
'incident_time': 'Approximate time when the incident occurred',
|
|
'patient_affected': 'Check if a patient was directly affected by this incident',
|
|
}
|
|
|
|
def clean_incident_date(self):
|
|
incident_date = self.cleaned_data.get('incident_date')
|
|
if incident_date and incident_date > timezone.now().date():
|
|
raise ValidationError('Incident date cannot be in the future.')
|
|
return incident_date
|
|
|
|
|
|
class RiskAssessmentForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating risk assessments.
|
|
"""
|
|
|
|
class Meta:
|
|
model = RiskAssessment
|
|
fields = [
|
|
'title', 'description', 'risk_category',
|
|
'likelihood', 'impact', 'current_controls',
|
|
'mitigation_plan', 'target_completion_date', 'responsible_person',
|
|
'review_notes'
|
|
]
|
|
widgets = {
|
|
'title': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Brief title describing the risk'
|
|
}),
|
|
'description': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 4,
|
|
'placeholder': 'Detailed description of the risk'
|
|
}),
|
|
# 'department': forms.TextInput(attrs={
|
|
# 'class': 'form-control',
|
|
# 'placeholder': 'Department or area affected'
|
|
# }),
|
|
'risk_category': forms.Select(attrs={'class': 'form-control'}),
|
|
'likelihood_score': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'min': '1',
|
|
'max': '5'
|
|
}),
|
|
'impact_score': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'min': '1',
|
|
'max': '5'
|
|
}),
|
|
'current_controls': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'What controls are currently in place?'
|
|
}),
|
|
'mitigation_plan': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'What actions will be taken to mitigate this risk?'
|
|
}),
|
|
'target_completion_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
'responsible_person': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Person responsible for mitigation actions'
|
|
}),
|
|
'review_notes': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'Additional review notes'
|
|
}),
|
|
}
|
|
help_texts = {
|
|
'likelihood': 'Rate the likelihood of this risk occurring (1=Very Low, 5=Very High)',
|
|
'impact_score': 'Rate the potential impact if this risk occurs (1=Very Low, 5=Very High)',
|
|
'target_completion_date': 'When should mitigation actions be completed?',
|
|
}
|
|
|
|
def clean_target_completion_date(self):
|
|
target_date = self.cleaned_data.get('target_completion_date')
|
|
if target_date and target_date <= timezone.now().date():
|
|
raise ValidationError('Target completion date must be in the future.')
|
|
return target_date
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
likelihood = cleaned_data.get('likelihood_score')
|
|
impact = cleaned_data.get('impact_score')
|
|
|
|
# Calculate risk level based on scores
|
|
if likelihood and impact:
|
|
risk_score = likelihood * impact
|
|
if risk_score >= 20:
|
|
cleaned_data['risk_level'] = 'CRITICAL'
|
|
elif risk_score >= 15:
|
|
cleaned_data['risk_level'] = 'HIGH'
|
|
elif risk_score >= 10:
|
|
cleaned_data['risk_level'] = 'MEDIUM'
|
|
else:
|
|
cleaned_data['risk_level'] = 'LOW'
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class AuditPlanForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating audit plans.
|
|
"""
|
|
|
|
class Meta:
|
|
model = AuditPlan
|
|
fields = [
|
|
'title', 'description', 'scope', 'audit_type', 'department',
|
|
'planned_start_date', 'planned_end_date', 'auditor',
|
|
'audit_team', 'criteria', 'objectives', 'notes'
|
|
]
|
|
widgets = {
|
|
'title': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Title of the audit'
|
|
}),
|
|
'description': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 4,
|
|
'placeholder': 'Detailed description of the plan'
|
|
}),
|
|
'scope': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'What areas/processes will be audited?'
|
|
}),
|
|
'audit_type': forms.Select(attrs={'class': 'form-control'}),
|
|
'department': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Department or area to be audited'
|
|
}),
|
|
'planned_start_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
'planned_end_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
'auditor': forms.Select(attrs={'class': 'form-control'}),
|
|
'audit_team': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'Names and roles of audit team members'
|
|
}),
|
|
'criteria': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Standards, policies, or criteria against which the audit will be conducted'
|
|
}),
|
|
'objectives': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'What are the objectives of this audit?'
|
|
}),
|
|
'notes': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'Additional notes'
|
|
}),
|
|
}
|
|
help_texts = {
|
|
'planned_start_date': 'When will the audit begin?',
|
|
'planned_end_date': 'When is the audit expected to be completed?',
|
|
'lead_auditor': 'Person responsible for leading this audit',
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
# Filter lead auditor by tenant
|
|
self.fields['lead_auditor'].queryset = User.objects.filter(
|
|
tenant=user.tenant,
|
|
is_active=True
|
|
).order_by('first_name', 'last_name')
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
start_date = cleaned_data.get('planned_start_date')
|
|
end_date = cleaned_data.get('planned_end_date')
|
|
|
|
if start_date and end_date:
|
|
if end_date <= start_date:
|
|
raise ValidationError('Planned end date must be after the start date.')
|
|
|
|
# Check for reasonable audit duration (not more than 6 months)
|
|
if (end_date - start_date).days > 180:
|
|
raise ValidationError('Audit duration cannot exceed 6 months.')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class AuditFindingForm(forms.ModelForm):
|
|
"""
|
|
Form for creating audit findings.
|
|
"""
|
|
|
|
class Meta:
|
|
model = AuditFinding
|
|
fields = [
|
|
'audit_plan', 'title', 'description', 'severity',
|
|
'finding_date', 'evidence', 'root_cause',
|
|
'corrective_actions', 'target_completion_date', 'responsible_person'
|
|
]
|
|
widgets = {
|
|
'audit_plan': forms.Select(attrs={'class': 'form-control'}),
|
|
'title': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Brief title describing the finding'
|
|
}),
|
|
'description': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 4,
|
|
'placeholder': 'Detailed description of the finding'
|
|
}),
|
|
'severity': forms.Select(attrs={'class': 'form-control'}),
|
|
'finding_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
# 'area_affected': forms.TextInput(attrs={
|
|
# 'class': 'form-control',
|
|
# 'placeholder': 'Area or process affected by this finding'
|
|
# }),
|
|
'evidence': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Evidence supporting this finding'
|
|
}),
|
|
'root_cause': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Analysis of the root cause(s)'
|
|
}),
|
|
'corrective_actions': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'Actions required to address this finding'
|
|
}),
|
|
'target_completion_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
'responsible_person': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Person responsible for corrective actions'
|
|
}),
|
|
}
|
|
help_texts = {
|
|
'severity': 'Select severity based on the impact of this finding',
|
|
'finding_date': 'Date when this finding was identified',
|
|
'target_completion_date': 'When should corrective actions be completed?',
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
# Filter audit plans by tenant and active status
|
|
self.fields['audit_plan'].queryset = AuditPlan.objects.filter(
|
|
tenant=user.tenant,
|
|
status__in=['PLANNED', 'IN_PROGRESS']
|
|
).order_by('-planned_start_date')
|
|
|
|
def clean_finding_date(self):
|
|
finding_date = self.cleaned_data.get('finding_date')
|
|
if finding_date and finding_date > timezone.now().date():
|
|
raise ValidationError('Finding date cannot be in the future.')
|
|
return finding_date
|
|
|
|
def clean_target_completion_date(self):
|
|
target_date = self.cleaned_data.get('target_completion_date')
|
|
if target_date and target_date <= timezone.now().date():
|
|
raise ValidationError('Target completion date must be in the future.')
|
|
return target_date
|
|
|
|
|
|
class ImprovementProjectForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating improvement projects.
|
|
"""
|
|
|
|
class Meta:
|
|
model = ImprovementProject
|
|
fields = [
|
|
'title', 'description', 'methodology', 'project_manager',
|
|
'actual_start_date', 'planned_end_date', 'actual_end_date','problem_statement',
|
|
'success_metrics', 'estimated_cost', 'actual_cost',
|
|
'resources_required', 'sponsor', 'notes'
|
|
]
|
|
# ['id', 'tenant', 'project_number', 'title', 'description', 'project_type', 'methodology', 'problem_statement',
|
|
# 'goal_statement', 'success_metrics', 'baseline_data', 'target_metrics', 'scope', 'project_manager', 'sponsor',
|
|
# 'department', 'planned_start_date', 'planned_end_date', 'actual_start_date', 'actual_end_date', 'status',
|
|
# 'phase', 'budget', 'actual_cost', 'roi_expected', 'roi_actual', 'lessons_learned', 'sustainability_plan',
|
|
# 'created_by', 'created_at', 'updated_at']
|
|
|
|
widgets = {
|
|
'title': forms.TextInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Name of the improvement project'
|
|
}),
|
|
'description': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 4,
|
|
'placeholder': 'Detailed description of the project'
|
|
}),
|
|
'methodology': forms.Select(attrs={'class': 'form-control'}),
|
|
'project_manager': forms.Select(attrs={'class': 'form-control'}),
|
|
'actual_start_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
'planned_end_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
'actual_end_date': forms.DateInput(attrs={
|
|
'class': 'form-control',
|
|
'type': 'date'
|
|
}),
|
|
'problem_statement': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'What problem is this project trying to solve?'
|
|
}),
|
|
'success_metrics': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 3,
|
|
'placeholder': 'How will success be measured?'
|
|
}),
|
|
'estimated_cost': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'step': '0.01',
|
|
'min': '0'
|
|
}),
|
|
'actual_cost': forms.NumberInput(attrs={
|
|
'class': 'form-control',
|
|
'step': '0.01',
|
|
'min': '0'
|
|
}),
|
|
'resources_required': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'What resources are needed for this project?'
|
|
}),
|
|
'sponsor': forms.Select(attrs={'class': 'form-control'}),
|
|
'project_team': forms.Select(attrs={'class': 'form-control'}),
|
|
'notes': forms.Textarea(attrs={
|
|
'class': 'form-control',
|
|
'rows': 2,
|
|
'placeholder': 'Additional notes'
|
|
}),
|
|
}
|
|
help_texts = {
|
|
'methodology': 'Quality improvement methodology to be used',
|
|
'start_date': 'When will the project begin?',
|
|
'target_completion_date': 'When is the project expected to be completed?',
|
|
'estimated_cost': 'Estimated cost of implementing this project',
|
|
'estimated_savings': 'Expected savings from this project',
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
# Filter project manager by tenant
|
|
self.fields['project_manager'].queryset = User.objects.filter(
|
|
tenant=user.tenant,
|
|
is_active=True
|
|
).order_by('first_name', 'last_name')
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
start_date = cleaned_data.get('start_date')
|
|
target_date = cleaned_data.get('target_completion_date')
|
|
|
|
if start_date and target_date:
|
|
if target_date <= start_date:
|
|
raise ValidationError('Target completion date must be after the start date.')
|
|
|
|
# Check for reasonable project duration (not more than 2 years)
|
|
if (target_date - start_date).days > 730:
|
|
raise ValidationError('Project duration cannot exceed 2 years.')
|
|
|
|
# Validate cost and savings
|
|
cost = cleaned_data.get('estimated_cost')
|
|
savings = cleaned_data.get('estimated_savings')
|
|
|
|
if cost and cost < 0:
|
|
raise ValidationError('Estimated cost cannot be negative.')
|
|
|
|
if savings and savings < 0:
|
|
raise ValidationError('Estimated savings cannot be negative.')
|
|
|
|
return cleaned_data
|
|
|