diff --git a/hypha/apply/projects/reports/models.py b/hypha/apply/projects/reports/models.py index c0ed2e90e2..0e07626f5a 100644 --- a/hypha/apply/projects/reports/models.py +++ b/hypha/apply/projects/reports/models.py @@ -280,13 +280,24 @@ def get_frequency_display(self): ) next_report = self.current_due_report() + # current_due_report() only returns an existing pending report row and + # can be None (e.g. the project hasn't started yet, or the next report + # row hasn't been created). Fall back to the schedule anchor date, which + # is what a report's end_date derives from, so the displayed schedule is + # still correct. + reference_date = ( + next_report.end_date + if next_report + else (self.schedule_start or self.project.proposed_start) + ) + if self.frequency == self.YEAR: if self.schedule_start and self.schedule_start.day == 31: day_of_month = _("last day") month = self.schedule_start.strftime("%B") else: - day_of_month = ordinal(next_report.end_date.day) - month = next_report.end_date.strftime("%B") + day_of_month = ordinal(reference_date.day) + month = reference_date.strftime("%B") if self.occurrence == 1: return _("Once a year on {month} {day}").format( day=day_of_month, month=month @@ -299,14 +310,14 @@ def get_frequency_display(self): if self.schedule_start and self.schedule_start.day == 31: day_of_month = _("last day") else: - day_of_month = ordinal(next_report.end_date.day) + day_of_month = ordinal(reference_date.day) if self.occurrence == 1: return _("Once a month on the {day}").format(day=day_of_month) return _("Every {occurrence} months on the {day}").format( occurrence=self.occurrence, day=day_of_month ) - weekday = next_report.end_date.strftime("%A") + weekday = reference_date.strftime("%A") if self.occurrence == 1: return _("Once a week on {weekday}").format(weekday=weekday) diff --git a/hypha/apply/projects/reports/tests/test_models.py b/hypha/apply/projects/reports/tests/test_models.py index 8afc585c0f..2fbd3e68fd 100644 --- a/hypha/apply/projects/reports/tests/test_models.py +++ b/hypha/apply/projects/reports/tests/test_models.py @@ -154,6 +154,41 @@ def test_submitted_report_unaffected(self): next_report = config.current_due_report() assert report != next_report + def test_frequency_display_without_pending_report_monthly(self): + """get_frequency_display must not crash when no pending report row exists. + + current_due_report() returns None in that case; the display should fall + back to the schedule_start anchor date. + """ + config = ReportConfigFactory( + disable_reporting=False, + frequency=ReportConfig.MONTH, + schedule_start=self.today.replace(day=15), + ) + assert config.current_due_report() is None + assert config.get_frequency_display() == "Once a month on the 15th" + + def test_frequency_display_without_pending_report_yearly(self): + config = ReportConfigFactory( + disable_reporting=False, + frequency=ReportConfig.YEAR, + schedule_start=self.today.replace(month=3, day=15), + ) + assert config.current_due_report() is None + assert config.get_frequency_display() == "Once a year on March 15th" + + def test_frequency_display_without_pending_report_weekly(self): + schedule_start = self.today.replace(day=15) + config = ReportConfigFactory( + disable_reporting=False, + frequency=ReportConfig.WEEK, + schedule_start=schedule_start, + ) + assert config.current_due_report() is None + assert config.get_frequency_display() == "Once a week on {weekday}".format( + weekday=schedule_start.strftime("%A") + ) + def test_past_due(self): """Test that past_due_reports includes overdue reports.""" report = ReportFactory(past_due=True)