diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py
index bfa925a2eb87c47bfe2e551df9d86535f8cbbd87..fce7c7bbc8cb10bd0b13412b0095b550a4958249 100644
--- a/opentech/apply/activity/messaging.py
+++ b/opentech/apply/activity/messaging.py
@@ -67,6 +67,7 @@ neat_related = {
     MESSAGES.SUBMIT_REPORT: 'report',
     MESSAGES.SKIPPED_REPORT: 'report',
     MESSAGES.REPORT_FREQUENCY_CHANGED: 'config',
+    MESSAGES.REPORT_NOTIFY: 'report',
 }
 
 
@@ -653,6 +654,7 @@ class EmailAdapter(AdapterBase):
         MESSAGES.SUBMIT_REPORT: 'messages/email/report_submitted.html',
         MESSAGES.SKIPPED_REPORT: 'messages/email/report_skipped.html',
         MESSAGES.REPORT_FREQUENCY_CHANGED: 'messages/email/report_frequency.html',
+        MESSAGES.REPORT_NOTIFY: 'messages/email/report_notify.html',
     }
 
     def get_subject(self, message_type, source):
diff --git a/opentech/apply/activity/migrations/0052_nullable_by_report_notify.py b/opentech/apply/activity/migrations/0052_nullable_by_report_notify.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f21463d7157fac539ddad71ca938002140a5678
--- /dev/null
+++ b/opentech/apply/activity/migrations/0052_nullable_by_report_notify.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.1.11 on 2019-11-05 16:20
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('activity', '0051_report_frequency_change'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='by',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterField(
+            model_name='event',
+            name='type',
+            field=models.CharField(choices=[('UPDATE_LEAD', 'Update Lead'), ('BATCH_UPDATE_LEAD', 'Batch Update Lead'), ('EDIT', 'Edit'), ('APPLICANT_EDIT', 'Applicant Edit'), ('NEW_SUBMISSION', 'New Submission'), ('SCREENING', 'Screening'), ('TRANSITION', 'Transition'), ('BATCH_TRANSITION', 'Batch Transition'), ('DETERMINATION_OUTCOME', 'Determination Outcome'), ('BATCH_DETERMINATION_OUTCOME', 'Batch Determination Outcome'), ('INVITED_TO_PROPOSAL', 'Invited To Proposal'), ('REVIEWERS_UPDATED', 'Reviewers Updated'), ('BATCH_REVIEWERS_UPDATED', 'Batch Reviewers Updated'), ('PARTNERS_UPDATED', 'Partners Updated'), ('PARTNERS_UPDATED_PARTNER', 'Partners Updated Partner'), ('READY_FOR_REVIEW', 'Ready For Review'), ('BATCH_READY_FOR_REVIEW', 'Batch Ready For Review'), ('NEW_REVIEW', 'New Review'), ('COMMENT', 'Comment'), ('PROPOSAL_SUBMITTED', 'Proposal Submitted'), ('OPENED_SEALED', 'Opened Sealed Submission'), ('REVIEW_OPINION', 'Review Opinion'), ('DELETE_SUBMISSION', 'Delete Submission'), ('DELETE_REVIEW', 'Delete Review'), ('CREATED_PROJECT', 'Created Project'), ('UPDATE_PROJECT_LEAD', 'Update Project Lead'), ('EDIT_REVIEW', 'Edit Review'), ('SEND_FOR_APPROVAL', 'Send for Approval'), ('APPROVE_PROJECT', 'Project was Approved'), ('PROJECT_TRANSITION', 'Project was Transitioned'), ('REQUEST_PROJECT_CHANGE', 'Project change requested'), ('UPLOAD_DOCUMENT', 'Document was Uploaded to Project'), ('REMOVE_DOCUMENT', 'Document was Removed from Project'), ('UPLOAD_CONTRACT', 'Contract was Uploaded to Project'), ('APPROVE_CONTRACT', 'Contract was Approved'), ('REQUEST_PAYMENT', 'Payment was requested for Project'), ('UPDATE_PAYMENT_REQUEST_STATUS', 'Updated Payment Request Status'), ('DELETE_PAYMENT_REQUEST', 'Delete Payment Request'), ('SENT_TO_COMPLIANCE', 'Project was sent to Compliance'), ('UPDATE_PAYMENT_REQUEST', 'Updated Payment Request'), ('SUBMIT_REPORT', 'Submit Report'), ('SKIPPED_REPORT', 'Skipped Report'), ('REPORT_FREQUENCY_CHANGED', 'Report Frequency Changed'), ('REPORT_NOTIFY', 'Report Notify')], max_length=50),
+        ),
+    ]
diff --git a/opentech/apply/activity/models.py b/opentech/apply/activity/models.py
index cf37ef0c242564a3c388305568f8686af32938c2..82b87a65445c2510d139a0696a9e6243a5ccd588 100644
--- a/opentech/apply/activity/models.py
+++ b/opentech/apply/activity/models.py
@@ -148,7 +148,7 @@ class Event(models.Model):
 
     when = models.DateTimeField(auto_now_add=True)
     type = models.CharField(choices=MESSAGES.choices(), max_length=50)
-    by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
+    by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True)
     content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE)
     object_id = models.PositiveIntegerField(blank=True, null=True)
     source = GenericForeignKey('content_type', 'object_id')
diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py
index 4e6aea6eb8a6c6e0593a9eba8462b940b46f9631..c4d891f483c2cfc853580ea5193df5e310a9c949 100644
--- a/opentech/apply/activity/options.py
+++ b/opentech/apply/activity/options.py
@@ -45,6 +45,7 @@ class MESSAGES(Enum):
     SUBMIT_REPORT = 'Submit Report'
     SKIPPED_REPORT = 'Skipped Report'
     REPORT_FREQUENCY_CHANGED = 'Report Frequency Changed'
+    REPORT_NOTIFY= 'Report Notify'
 
     @classmethod
     def choices(cls):
diff --git a/opentech/apply/activity/templates/messages/email/report_notify.html b/opentech/apply/activity/templates/messages/email/report_notify.html
new file mode 100644
index 0000000000000000000000000000000000000000..104285b756e07a53ac11b581c380d72ce4d4834c
--- /dev/null
+++ b/opentech/apply/activity/templates/messages/email/report_notify.html
@@ -0,0 +1,10 @@
+{% extends "messages/email/applicant_base.html" %}
+
+{% block content %}
+
+A report is due for {{ source.title }} on {{ report.end_date }}.
+
+More information can be found on the project page:
+{{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}
+
+{% endblock %}
diff --git a/opentech/apply/projects/management/commands/notify_report_due.py b/opentech/apply/projects/management/commands/notify_report_due.py
index d0162caed2ce85338bd212f546daaff1f0ee59f0..a74e3c572c946a6ad28a28a8c18ec46a57c7e2ee 100644
--- a/opentech/apply/projects/management/commands/notify_report_due.py
+++ b/opentech/apply/projects/management/commands/notify_report_due.py
@@ -1,8 +1,13 @@
 from dateutil.relativedelta import relativedelta
 
+from django.conf import settings
+from django.contrib.messages.storage.fallback import FallbackStorage
 from django.core.management.base import BaseCommand
+from django.http import HttpRequest
 from django.utils import timezone
 
+from opentech.apply.activity.messaging import MESSAGES, messenger
+from opentech.apply.home.models import ApplyHomePage
 from opentech.apply.projects.models import Project
 
 
@@ -13,11 +18,37 @@ class Command(BaseCommand):
         parser.add_argument('days', type=int)
 
     def handle(self, *args, **options):
-        due_date = timezone.now().date() + relativedelta(days=options['days'])
+        site = ApplyHomePage.objects.first().get_site()
+
+        # Mock a HTTPRequest in order to pass the site settings into the
+        # templates
+        request = HttpRequest()
+        request.META['SERVER_NAME'] = site.hostname
+        request.META['SERVER_PORT'] = site.port
+        request.META[settings.SECURE_PROXY_SSL_HEADER] = 'https'
+        request.session = {}
+        request._messages = FallbackStorage(request)
+
+        today = timezone.now().date()
+        due_date = today + relativedelta(days=options['days'])
         for project in Project.objects.in_progress():
             next_report = project.report_config.current_due_report()
-            if next_report.end_date == due_date:
+            due_soon = next_report.end_date == due_date
+            not_notified_today = (
+                not next_report.notified or
+                next_report.notified.date() != today
+            )
+            if due_soon and not_notified_today:
+                messenger(
+                    MESSAGES.REPORT_NOTIFY,
+                    request=request,
+                    user=None,
+                    source=project,
+                    related=next_report,
+                )
                 # Notify about the due report
+                next_report.notified = timezone.now()
+                next_report.save()
                 self.stdout.write(
                     self.style.SUCCESS(f'Notified project: {project.id}')
                 )
diff --git a/opentech/apply/projects/migrations/0032_report_notified.py b/opentech/apply/projects/migrations/0032_report_notified.py
new file mode 100644
index 0000000000000000000000000000000000000000..ec7ae7bf0c846eb290df9950923434cc01a237bc
--- /dev/null
+++ b/opentech/apply/projects/migrations/0032_report_notified.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.11 on 2019-11-05 15:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('application_projects', '0031_add_public_private_content'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='report',
+            name='notified',
+            field=models.DateTimeField(null=True),
+        ),
+    ]
diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py
index c652a2a24c6f86a6b2198f19c6a388bce48b08e5..87993c947a3acc4199eb10db0405248498040bc5 100644
--- a/opentech/apply/projects/models.py
+++ b/opentech/apply/projects/models.py
@@ -746,6 +746,7 @@ class Report(models.Model):
     end_date = models.DateField()
     project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="reports")
     submitted = models.DateTimeField(null=True)
+    notified = models.DateTimeField(null=True)
     current = models.OneToOneField(
         "ReportVersion",
         on_delete=models.CASCADE,
diff --git a/opentech/apply/projects/tests/factories.py b/opentech/apply/projects/tests/factories.py
index a585a8b0b9d772df05f3d187b48e83f9a4615083..05a63a64d916310fc0569e805e9c2b1d81e8e481 100644
--- a/opentech/apply/projects/tests/factories.py
+++ b/opentech/apply/projects/tests/factories.py
@@ -8,6 +8,7 @@ from django.utils import timezone
 
 from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory
 from opentech.apply.projects.models import (
+    COMPLETE,
     Contract,
     DocumentCategory,
     IN_PROGRESS,
@@ -101,6 +102,9 @@ class ProjectFactory(factory.DjangoModelFactory):
         in_progress = factory.Trait(
             status=IN_PROGRESS,
         )
+        is_complete = factory.Trait(
+            status=COMPLETE,
+        )
 
 
 class ContractFactory(factory.DjangoModelFactory):
diff --git a/opentech/apply/projects/tests/test_commands.py b/opentech/apply/projects/tests/test_commands.py
index b0ee1023a533aab5e979bdeee137f09d3cbe1121..5897e1c1be0c6a543e2072504377e497cc4b6cc0 100644
--- a/opentech/apply/projects/tests/test_commands.py
+++ b/opentech/apply/projects/tests/test_commands.py
@@ -3,9 +3,11 @@ from io import StringIO
 from dateutil.relativedelta import relativedelta
 
 from django.core.management import call_command
-from django.test import TestCase
+from django.test import override_settings, TestCase
 from django.utils import timezone
 
+from opentech.apply.home.models import ApplyHomePage
+
 from .factories import (
     ProjectFactory,
     ReportConfigFactory,
@@ -13,6 +15,8 @@ from .factories import (
 )
 
 
+@override_settings(ALLOWED_HOSTS=[ApplyHomePage.objects.first().get_site().hostname])
+@override_settings(ROOT_URLCONF='opentech.apply.urls')
 class TestNotifyReportDue(TestCase):
     def test_notify_report_due_in_7_days(self):
         in_a_week = timezone.now() + relativedelta(days=7)
@@ -23,7 +27,7 @@ class TestNotifyReportDue(TestCase):
 
     def test_dont_notify_report_due_in_7_days_already_submitted(self):
         in_a_week = timezone.now() + relativedelta(days=7)
-        config = ReportConfigFactory(schedule_start=in_a_week)
+        config = ReportConfigFactory(schedule_start=in_a_week, project__in_progress=True)
         ReportFactory(
             project=config.project,
             is_submitted=True,
@@ -33,14 +37,26 @@ class TestNotifyReportDue(TestCase):
         call_command('notify_report_due', 7, stdout=out)
         self.assertNotIn('Notified project', out.getvalue())
 
+    def test_dont_notify_already_notified(self):
+        in_a_week = timezone.now() + relativedelta(days=7)
+        config = ReportConfigFactory(schedule_start=in_a_week, project__in_progress=True)
+        ReportFactory(
+            project=config.project,
+            end_date=config.schedule_start,
+            notified=timezone.now(),
+        )
+        out = StringIO()
+        call_command('notify_report_due', 7, stdout=out)
+        self.assertNotIn('Notified project', out.getvalue())
+
     def test_dont_notify_project_not_in_progress(self):
         ProjectFactory()
         out = StringIO()
         call_command('notify_report_due', 7, stdout=out)
         self.assertNotIn('Notified project', out.getvalue())
 
-    def test_dont_notify_project_closed(self):
-        ProjectFactory()
+    def test_dont_notify_project_complete(self):
+        ProjectFactory(is_complete=True)
         out = StringIO()
         call_command('notify_report_due', 7, stdout=out)
         self.assertNotIn('Notified project', out.getvalue())