Skip to content
Merged
1 change: 1 addition & 0 deletions hypha/apply/funds/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ def test_can_create_project(self):
"project_create_form": "",
"project_lead": self.user.id,
"project_initial_status": CONTRACTING,
"project_end": timezone.now().date(),
"submission": self.submission.id,
},
view_name="create_project",
Expand Down
2 changes: 1 addition & 1 deletion hypha/apply/funds/views/submission_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ def post(self, *args, **kwargs):
return render(
self.request,
"funds/modals/create_project_form.html",
context={"form": form, "value": _("Confirm"), "object": self.object},
context={"form": form, "value": _("Confirm"), "object": self.submission},
status=400,
)

Expand Down
2 changes: 2 additions & 0 deletions hypha/apply/projects/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
SkipPAFApprovalProcessForm,
StaffUploadContractForm,
SubmitContractDocumentsForm,
UpdateProjectDatesForm,
UpdateProjectLeadForm,
UpdateProjectTitleForm,
UploadContractDocumentForm,
Expand All @@ -44,6 +45,7 @@
"UploadContractDocumentForm",
"StaffUploadContractForm",
"UploadDocumentForm",
"UpdateProjectDatesForm",
"UpdateProjectLeadForm",
"CreateInvoiceForm",
"ChangeInvoiceStatusForm",
Expand Down
56 changes: 53 additions & 3 deletions hypha/apply/projects/forms/project.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from datetime import date

from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Count, Q
from django.utils.text import slugify
Expand All @@ -10,6 +13,7 @@
get_project_default_status,
get_project_status_options,
)
from hypha.apply.projects.templatetags.project_tags import show_start_date
from hypha.apply.stream_forms.fields import SingleFileField
from hypha.apply.stream_forms.forms import StreamBaseForm
from hypha.apply.users.roles import STAFF_GROUP_NAME
Expand Down Expand Up @@ -86,16 +90,20 @@ class ProjectCreateForm(forms.Form):
)

project_lead = forms.ModelChoiceField(
label=_("Select Project Lead"), queryset=User.objects.all()
label=_("Select project lead"), queryset=User.objects.all()
)

# Set the initial value to the settings default if valid, otherwise fall back to draft
project_initial_status = forms.ChoiceField(
label=_("Initial Project Status"),
label=_("Initial project status"),
choices=get_project_status_options(),
initial=get_project_default_status(),
)

project_end = forms.DateField(
label=_("Project end date"),
)

def __init__(self, *args, instance=None, **kwargs):
super().__init__(*args, **kwargs)

Expand All @@ -119,7 +127,24 @@ def save(self, *args, **kwargs):
submission = self.cleaned_data["submission"]
lead = self.cleaned_data["project_lead"]
status = self.cleaned_data["project_initial_status"]
return Project.create_from_submission(submission, lead=lead, status=status)
end_date = self.cleaned_data["project_end"]

start_date = None

if not settings.PROJECTS_START_AFTER_CONTRACTING or status in [
INVOICING_AND_REPORTING,
CLOSING,
COMPLETE,
]:
start_date = date.today()

return Project.create_from_submission(
submission,
lead=lead,
status=status,
end_date=end_date,
start_date=start_date,
)


class MixedMetaClass(type(StreamBaseForm), type(forms.ModelForm)):
Expand Down Expand Up @@ -444,3 +469,28 @@ class Meta:

def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)


class UpdateProjectDatesForm(forms.ModelForm):
class Meta:
fields = ["proposed_start", "proposed_end"]
model = Project

def clean(self):
cleaned_data = super().clean()
if (
show_start_date(self.instance)
and cleaned_data["proposed_start"] >= cleaned_data["proposed_end"]
):
self.add_error(
"proposed_end", _("The end date must be after the start date.")
)

def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)
# Only show the start date field if relevant
if not show_start_date(self.instance):
proposed_start = self.fields["proposed_start"]
proposed_start.disabled = True
proposed_start.required = False
proposed_start.widget = proposed_start.hidden_widget()
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.20 on 2025-04-10 17:27

import datetime
from django.db import migrations, models


def migration_set_project_start_date(apps, schema_editor):
Project = apps.get_model("application_projects", "Project")
for project in Project.objects.all():
if not project.proposed_start:
project.proposed_start = project.created_at
project.save(update_fields=["proposed_start"])


class Migration(migrations.Migration):
dependencies = [
("application_projects", "0099_remove_reportconfig_project_and_more"),
]

operations = [
migrations.RunPython(migration_set_project_start_date),
migrations.AlterField(
model_name="project",
name="proposed_end",
field=models.DateField(null=True, verbose_name="Proposed end date"),
),
migrations.AlterField(
model_name="project",
name="proposed_start",
field=models.DateField(
default=datetime.date.today,
null=True,
verbose_name="Proposed start date",
),
),
]
42 changes: 12 additions & 30 deletions hypha/apply/projects/models/project.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from datetime import date

from django import forms
from django.apps import apps
Expand All @@ -20,7 +21,7 @@
Value,
When,
)
from django.db.models.functions import Cast, Coalesce
from django.db.models.functions import Coalesce
from django.db.models.signals import post_delete
from django.dispatch.dispatcher import receiver
from django.urls import reverse
Expand Down Expand Up @@ -164,21 +165,6 @@ def with_outstanding_reports(self):
)
)

def with_start_date(self):
return self.annotate(
start=Cast(
Subquery(
Contract.objects.filter(
project=OuterRef("pk"),
)
.approved()
.order_by("approved_at")
.values("approved_at")[:1]
),
models.DateField(),
)
)

def for_table(self):
return (
self.with_amount_paid()
Expand Down Expand Up @@ -259,8 +245,10 @@ class Project(BaseStreamForm, AccessFormData, models.Model):
decimal_places=2,
validators=[MinValueValidator(limit_value=0)],
)
proposed_start = models.DateTimeField(_("Proposed Start Date"), null=True)
proposed_end = models.DateTimeField(_("Proposed End Date"), null=True)
proposed_start = models.DateField(
_("Proposed start date"), null=True, default=date.today
)
proposed_end = models.DateField(_("Proposed end date"), null=True)

status = models.TextField(choices=PROJECT_STATUS_CHOICES, default=DRAFT)

Expand Down Expand Up @@ -321,7 +309,9 @@ def get_address_display(self):
return "" # todo: need to figure out

@classmethod
def create_from_submission(cls, submission, lead=None, status=None):
def create_from_submission(
cls, submission, lead=None, status=None, end_date=None, start_date=None
):
"""
Create a Project from the given submission.

Expand Down Expand Up @@ -357,26 +347,18 @@ def create_from_submission(cls, submission, lead=None, status=None):
title=submission.title,
status=status,
lead=lead if lead else None,
proposed_end=end_date,
proposed_start=start_date,
value=submission.form_data.get("value", 0),
)

@property
def start_date(self):
# Assume project starts when OTF are happy with the first signed contract
first_approved_contract = (
self.contracts.approved().order_by("approved_at").first()
)
if not first_approved_contract:
return None
return first_approved_contract.approved_at.date()

@property
def end_date(self):
# Aiming for the proposed end date as the last day of the project
# If still ongoing assume today is the end
if self.proposed_end:
return max(
self.proposed_end.date(),
self.proposed_end,
timezone.now().date(),
)
return timezone.now().date()
Expand Down
10 changes: 4 additions & 6 deletions hypha/apply/projects/reports/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ def for_table(self):
project_start_date=Subquery(
Project.objects.filter(
pk=OuterRef("project_id"),
)
.with_start_date()
.values("start")[:1]
).values("proposed_start")[:1]
),
start=Case(
When(
Expand Down Expand Up @@ -176,7 +174,7 @@ def start_date(self):
if last_report:
return last_report.end_date + relativedelta(days=1)

return self.project.start_date
return self.project.proposed_start


class ReportVersion(BaseStreamForm, AccessFormData, models.Model):
Expand Down Expand Up @@ -329,14 +327,14 @@ def current_due_report(self):
return None

# Project not started - no reporting required
if not self.project.start_date:
if not self.project.proposed_start:
return None

today = timezone.now().date()

last_report = self.last_report()

schedule_date = self.schedule_start or self.project.start_date
schedule_date = self.schedule_start or self.project.proposed_start

if last_report:
# Frequency is one time and last report exists - no reporting required anymore
Expand Down
4 changes: 3 additions & 1 deletion hypha/apply/projects/reports/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def test_no_report_creates_report(self):
# combined => 31th + 1 month = 30th - 1 day = 29th (wrong)
# separate => 31th - 1 day = 30th + 1 month = 30th (correct)
next_due = (
report.project.start_date - relativedelta(days=1) + relativedelta(months=1)
report.project.proposed_start
- relativedelta(days=1)
+ relativedelta(months=1)
)
assert Report.objects.count() == 1
assert report.end_date == next_due
Expand Down
2 changes: 1 addition & 1 deletion hypha/apply/projects/reports/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ def get_form_kwargs(self, **kwargs):
}
else:
kwargs["initial"] = {
"start": self.project.start_date,
"start": self.project.proposed_start,
}
return kwargs

Expand Down
9 changes: 5 additions & 4 deletions hypha/apply/projects/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ class BaseProjectsTable(tables.Table):
fund = tables.Column(verbose_name=_("Fund"), accessor="submission__page")
reporting = tables.Column(verbose_name=_("Reporting"), accessor="pk")
last_payment_request = tables.DateColumn()
end_date = tables.DateColumn(verbose_name=_("End date"), accessor="proposed_end")

def order_reporting(self, qs, is_descending):
direction = "-" if is_descending else ""
Expand Down Expand Up @@ -176,7 +177,7 @@ class Meta:
"fund",
"reporting",
"last_payment_request",
"created_at",
"end_date",
]
model = Project
template_name = "application_projects/tables/table.html"
Expand All @@ -192,7 +193,7 @@ class Meta:
"lead",
"reporting",
"last_payment_request",
"created_at",
"end_date",
]
model = Project
orderable = False
Expand Down Expand Up @@ -254,10 +255,10 @@ class Meta:
"fund",
"reporting",
"last_payment_request",
"created_at",
"end_date",
]
model = Project
orderable = True
order_by = ("-created_at",)
order_by = ("end_date",)
template_name = "application_projects/tables/table.html"
attrs = {"class": "projects-table"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% load i18n %}
{% modal_title %}{% trans "Update project dates" %}{% endmodal_title %}

<div class="p-4">
{% include 'includes/dialog_form_base.html' with form=form value=value %}
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an important detail at all but the date pickers are different widths - not sure if this can be fixed here because of the include template though

Screenshot 2025-04-17 at 09 42 49

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was custom css to change the width of the last date field, been there for years.

Removed it and we will see if it mess something up, likely not.

Loading
Loading