From d7a897feb709b389269bf408f22b63a33a00731b Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Thu, 10 Apr 2025 16:17:21 +0200 Subject: [PATCH 01/12] Set project start date to now and add field in create project form to set the end date. Show dates on project detail view. --- hypha/apply/projects/forms/project.py | 13 ++++++++++--- hypha/apply/projects/models/project.py | 5 +++-- .../application_projects/project_detail.html | 10 ++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py index 3e9e01a02e..99d9252fc6 100644 --- a/hypha/apply/projects/forms/project.py +++ b/hypha/apply/projects/forms/project.py @@ -86,16 +86,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) @@ -119,7 +123,10 @@ 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"] + return Project.create_from_submission( + submission, lead=lead, status=status, end_date=end_date + ) class MixedMetaClass(type(StreamBaseForm), type(forms.ModelForm)): diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index f13a1129a1..9e8615ad27 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -259,7 +259,7 @@ class Project(BaseStreamForm, AccessFormData, models.Model): decimal_places=2, validators=[MinValueValidator(limit_value=0)], ) - proposed_start = models.DateTimeField(_("Proposed Start Date"), null=True) + proposed_start = models.DateTimeField(_("Proposed Start Date"), auto_now_add=True) proposed_end = models.DateTimeField(_("Proposed End Date"), null=True) status = models.TextField(choices=PROJECT_STATUS_CHOICES, default=DRAFT) @@ -321,7 +321,7 @@ 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): """ Create a Project from the given submission. @@ -357,6 +357,7 @@ 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, value=submission.form_data.get("value", 0), ) diff --git a/hypha/apply/projects/templates/application_projects/project_detail.html b/hypha/apply/projects/templates/application_projects/project_detail.html index c5ed6027b6..c6f0a58d3c 100644 --- a/hypha/apply/projects/templates/application_projects/project_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_detail.html @@ -70,6 +70,16 @@
{% trans "E-mail" %}
- {% endif %} + +
+
{% trans "Start date" %}
+

{{ object.proposed_start|date:"SHORT_DATETIME_FORMAT"|default:"-" }}

+
+ +
+
{% trans "End date" %}
+

{{ object.proposed_end|date:"SHORT_DATETIME_FORMAT"|default:"-" }}

+
{% user_can_view_invoices object user as can_view_invoices %} From e353bb8d00465c034f0cca98769b724f4ad6fd37 Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Thu, 10 Apr 2025 17:36:00 +0200 Subject: [PATCH 02/12] Add migration. --- ...100_alter_project_proposed_end_and_more.py | 33 +++++++++++++++++++ hypha/apply/projects/models/project.py | 4 +-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 hypha/apply/projects/migrations/0100_alter_project_proposed_end_and_more.py diff --git a/hypha/apply/projects/migrations/0100_alter_project_proposed_end_and_more.py b/hypha/apply/projects/migrations/0100_alter_project_proposed_end_and_more.py new file mode 100644 index 0000000000..e565381260 --- /dev/null +++ b/hypha/apply/projects/migrations/0100_alter_project_proposed_end_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.20 on 2025-04-10 15:27 + +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.DateTimeField(null=True, verbose_name="Proposed end date"), + ), + migrations.AlterField( + model_name="project", + name="proposed_start", + field=models.DateTimeField( + auto_now_add=True, verbose_name="Proposed start date" + ), + ), + ] diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index 9e8615ad27..a142bbfa8e 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -259,8 +259,8 @@ class Project(BaseStreamForm, AccessFormData, models.Model): decimal_places=2, validators=[MinValueValidator(limit_value=0)], ) - proposed_start = models.DateTimeField(_("Proposed Start Date"), auto_now_add=True) - proposed_end = models.DateTimeField(_("Proposed End Date"), null=True) + proposed_start = models.DateTimeField(_("Proposed start date"), auto_now_add=True) + proposed_end = models.DateTimeField(_("Proposed end date"), null=True) status = models.TextField(choices=PROJECT_STATUS_CHOICES, default=DRAFT) From 348df297b9eab79424586e23fc405cbeb66376cf Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Thu, 10 Apr 2025 19:33:21 +0200 Subject: [PATCH 03/12] Make it possible to edit the project dates. --- hypha/apply/projects/forms/__init__.py | 2 + hypha/apply/projects/forms/project.py | 9 ++++ ...100_alter_project_proposed_end_and_more.py | 11 +++-- hypha/apply/projects/models/project.py | 7 ++- .../modals/project_dates_update.html | 6 +++ .../partials/project_information.html | 47 +++++++++++++++++++ .../{project-lead.html => project_lead.html} | 0 .../application_projects/project_detail.html | 39 +++++---------- hypha/apply/projects/urls.py | 12 +++++ hypha/apply/projects/views/__init__.py | 4 ++ hypha/apply/projects/views/project.py | 37 +++++++++++++++ .../apply/projects/views/project_partials.py | 12 ++++- 12 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 hypha/apply/projects/templates/application_projects/modals/project_dates_update.html create mode 100644 hypha/apply/projects/templates/application_projects/partials/project_information.html rename hypha/apply/projects/templates/application_projects/partials/{project-lead.html => project_lead.html} (100%) diff --git a/hypha/apply/projects/forms/__init__.py b/hypha/apply/projects/forms/__init__.py index a0fd67814a..85642d0089 100644 --- a/hypha/apply/projects/forms/__init__.py +++ b/hypha/apply/projects/forms/__init__.py @@ -18,6 +18,7 @@ SkipPAFApprovalProcessForm, StaffUploadContractForm, SubmitContractDocumentsForm, + UpdateProjectDatesForm, UpdateProjectLeadForm, UpdateProjectTitleForm, UploadContractDocumentForm, @@ -44,6 +45,7 @@ "UploadContractDocumentForm", "StaffUploadContractForm", "UploadDocumentForm", + "UpdateProjectDatesForm", "UpdateProjectLeadForm", "CreateInvoiceForm", "ChangeInvoiceStatusForm", diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py index 99d9252fc6..c5a91fce2c 100644 --- a/hypha/apply/projects/forms/project.py +++ b/hypha/apply/projects/forms/project.py @@ -451,3 +451,12 @@ 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 __init__(self, *args, user=None, **kwargs): + super().__init__(*args, **kwargs) diff --git a/hypha/apply/projects/migrations/0100_alter_project_proposed_end_and_more.py b/hypha/apply/projects/migrations/0100_alter_project_proposed_end_and_more.py index e565381260..b9d30c4cef 100644 --- a/hypha/apply/projects/migrations/0100_alter_project_proposed_end_and_more.py +++ b/hypha/apply/projects/migrations/0100_alter_project_proposed_end_and_more.py @@ -1,5 +1,6 @@ -# Generated by Django 4.2.20 on 2025-04-10 15:27 +# Generated by Django 4.2.20 on 2025-04-10 17:27 +import datetime from django.db import migrations, models @@ -21,13 +22,15 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="project", name="proposed_end", - field=models.DateTimeField(null=True, verbose_name="Proposed end date"), + field=models.DateField(null=True, verbose_name="Proposed end date"), ), migrations.AlterField( model_name="project", name="proposed_start", - field=models.DateTimeField( - auto_now_add=True, verbose_name="Proposed start date" + field=models.DateField( + default=datetime.date.today, + null=True, + verbose_name="Proposed start date", ), ), ] diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index a142bbfa8e..dfea79f481 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -1,4 +1,5 @@ import logging +from datetime import date from django import forms from django.apps import apps @@ -259,8 +260,10 @@ class Project(BaseStreamForm, AccessFormData, models.Model): decimal_places=2, validators=[MinValueValidator(limit_value=0)], ) - proposed_start = models.DateTimeField(_("Proposed start date"), auto_now_add=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) diff --git a/hypha/apply/projects/templates/application_projects/modals/project_dates_update.html b/hypha/apply/projects/templates/application_projects/modals/project_dates_update.html new file mode 100644 index 0000000000..7a3248e18b --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/modals/project_dates_update.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% modal_title %}{% trans "Update project dates" %}{% endmodal_title %} + +
+ {% include 'includes/dialog_form_base.html' with form=form value=value %} +
diff --git a/hypha/apply/projects/templates/application_projects/partials/project_information.html b/hypha/apply/projects/templates/application_projects/partials/project_information.html new file mode 100644 index 0000000000..40f262183d --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/partials/project_information.html @@ -0,0 +1,47 @@ +{% load i18n heroicons %} + +
+
{% trans "Contractor" %}
+

{{ object.user |default:"-" }}

+
+ +
+
{% trans "E-mail" %}
+ {% if object.user.email %} + {{ object.user.email }} + {% else %} + - + {% endif %} +
+ +
+
{% trans "Start date" %}
+

{{ object.proposed_start|date:"SHORT_DATE_FORMAT"|default:"-" }}

+
+ +
+
+ {% trans "End date" %} + {% if request.user.is_apply_staff %} + + {% heroicon_solid "pencil-square" class="inline mt-2 ms-1" aria_hidden=true %} + {% trans "edit dates" %} + + {% endif %} +
+

{{ object.proposed_end|date:"SHORT_DATE_FORMAT"|default:"-" }}

+
+ + + + + + + + + + diff --git a/hypha/apply/projects/templates/application_projects/partials/project-lead.html b/hypha/apply/projects/templates/application_projects/partials/project_lead.html similarity index 100% rename from hypha/apply/projects/templates/application_projects/partials/project-lead.html rename to hypha/apply/projects/templates/application_projects/partials/project_lead.html diff --git a/hypha/apply/projects/templates/application_projects/project_detail.html b/hypha/apply/projects/templates/application_projects/project_detail.html index c6f0a58d3c..82b97752bf 100644 --- a/hypha/apply/projects/templates/application_projects/project_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_detail.html @@ -56,30 +56,15 @@

{% trans "Project Information" %}

-
-
-
{% trans "Contractor" %}
-

{{ object.user |default:"-" }}

-
- -
-
{% trans "E-mail" %}
- {% if object.user.email %} - {{ object.user.email }} - {% else %} - - - {% endif %} -
- -
-
{% trans "Start date" %}
-

{{ object.proposed_start|date:"SHORT_DATETIME_FORMAT"|default:"-" }}

-
- -
-
{% trans "End date" %}
-

{{ object.proposed_end|date:"SHORT_DATETIME_FORMAT"|default:"-" }}

-
+
+ + + +
{% user_can_view_invoices object user as can_view_invoices %} @@ -109,9 +94,9 @@
{% trans "End date" %}
{% user_next_step_on_project object user request=request as next_step %} {% if next_step %}
-
{% trans "Start date" %}
+
+ {% trans "Start date" %} + {% if request.user.is_apply_staff %} + + {% heroicon_solid "pencil-square" class="inline mt-2 align-text-bottom ms-1" aria_hidden=true size=20 %} + {% trans "edit dates" %} + + {% endif %} +

{{ object.proposed_start|date:"SHORT_DATE_FORMAT"|default:"-" }}

@@ -28,20 +40,10 @@
hx-get="{% url 'apply:projects:project_dates_update' object.submission.id %}" hx-target="#htmx-modal" > - {% heroicon_solid "pencil-square" class="inline mt-2 ms-1" aria_hidden=true %} + {% heroicon_solid "pencil-square" class="inline mt-2 align-text-bottom ms-1" aria_hidden=true size=20 %} {% trans "edit dates" %} {% endif %}

{{ object.proposed_end|date:"SHORT_DATE_FORMAT"|default:"-" }}

- - - - - - - - - - diff --git a/hypha/static_src/sass/components/_form.scss b/hypha/static_src/sass/components/_form.scss index 5789f1a18b..b8a984d3f4 100644 --- a/hypha/static_src/sass/components/_form.scss +++ b/hypha/static_src/sass/components/_form.scss @@ -88,10 +88,6 @@ } } } - - input[type="date"]:last-child { - max-width: 385px; - } } &__question { From 8b82da1b1e48845b4b7429dadc8f0f768904a2c6 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Fri, 18 Apr 2025 18:22:12 -0400 Subject: [PATCH 09/12] Removed `start_date` in favor of using `proposed_start`, fixed reports --- hypha/apply/projects/forms/project.py | 19 ++++++++++- hypha/apply/projects/models/project.py | 32 +++---------------- hypha/apply/projects/reports/models.py | 10 +++--- .../projects/reports/tests/test_models.py | 2 +- hypha/apply/projects/reports/views.py | 2 +- hypha/apply/projects/views/project.py | 3 ++ hypha/settings/base.py | 7 ++-- 7 files changed, 37 insertions(+), 38 deletions(-) diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py index c5a91fce2c..4c9913f64e 100644 --- a/hypha/apply/projects/forms/project.py +++ b/hypha/apply/projects/forms/project.py @@ -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 @@ -124,8 +127,22 @@ def save(self, *args, **kwargs): lead = self.cleaned_data["project_lead"] status = self.cleaned_data["project_initial_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 + submission, + lead=lead, + status=status, + end_date=end_date, + start_date=start_date, ) diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index dfea79f481..b8b3fa65d9 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -21,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 @@ -165,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() @@ -324,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, end_date=None): + def create_from_submission( + cls, submission, lead=None, status=None, end_date=None, start_date=None + ): """ Create a Project from the given submission. @@ -361,19 +348,10 @@ def create_from_submission(cls, submission, lead=None, status=None, end_date=Non 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 diff --git a/hypha/apply/projects/reports/models.py b/hypha/apply/projects/reports/models.py index 7a8dd7eb91..dc88876c63 100644 --- a/hypha/apply/projects/reports/models.py +++ b/hypha/apply/projects/reports/models.py @@ -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( @@ -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): @@ -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 diff --git a/hypha/apply/projects/reports/tests/test_models.py b/hypha/apply/projects/reports/tests/test_models.py index 69c3abe5f8..b34c8ae7a1 100644 --- a/hypha/apply/projects/reports/tests/test_models.py +++ b/hypha/apply/projects/reports/tests/test_models.py @@ -80,7 +80,7 @@ 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 diff --git a/hypha/apply/projects/reports/views.py b/hypha/apply/projects/reports/views.py index c8b194be43..913d131433 100644 --- a/hypha/apply/projects/reports/views.py +++ b/hypha/apply/projects/reports/views.py @@ -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 diff --git a/hypha/apply/projects/views/project.py b/hypha/apply/projects/views/project.py index 837bd41b43..2de5ff0aaf 100644 --- a/hypha/apply/projects/views/project.py +++ b/hypha/apply/projects/views/project.py @@ -739,6 +739,9 @@ def post(self, *args, **kwargs): self.project.save(update_fields=["status"]) old_stage = CONTRACTING + if settings.PROJECTS_START_AFTER_CONTRACTING: + self.project.proposed_start = datetime.date.today() + messenger( MESSAGES.PROJECT_TRANSITION, request=self.request, diff --git a/hypha/settings/base.py b/hypha/settings/base.py index 1810932838..afd2adaa75 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -85,6 +85,8 @@ ORG_SHORT_NAME = env.str("ORG_SHORT_NAME", "ACME") ORG_URL = env.str("ORG_URL", "https://www.example.org/") +# Project settings. + # Enable Projects in Hypha. Contracts and invoicing that comes after a submission is approved. PROJECTS_ENABLED = env.bool("PROJECTS_ENABLED", False) @@ -95,6 +97,9 @@ # Will be used for auto-create or be the default selection in the project creation form PROJECTS_DEFAULT_STATUS = env.str("PROJECTS_DEFAULT_STATUS", "draft") +# When enabled, the project start date will be set and displayed after the contracting phase, if disabled it is set on project creation +PROJECTS_START_AFTER_CONTRACTING = env.bool("PROJECTS_START_AFTER_CONTRACTING", True) + # Send out e-mail, slack messages etc. from Hypha. Set to true for production. SEND_MESSAGES = env.bool("SEND_MESSAGES", False) @@ -166,8 +171,6 @@ # Require an applicant to view their rendered application before submitting SUBMISSION_PREVIEW_REQUIRED = env.bool("SUBMISSION_PREVIEW_REQUIRED", True) -# Project settings. - # SECRET_KEY is required SECRET_KEY = env.str("SECRET_KEY", None) From 424039e394fb6e25f713f6925b3dc63668b41c3b Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Mon, 21 Apr 2025 15:47:11 -0400 Subject: [PATCH 10/12] Added date call to project factory --- hypha/apply/projects/tests/factories.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hypha/apply/projects/tests/factories.py b/hypha/apply/projects/tests/factories.py index 243111058f..b78e15c681 100644 --- a/hypha/apply/projects/tests/factories.py +++ b/hypha/apply/projects/tests/factories.py @@ -98,8 +98,8 @@ class ProjectFactory(factory.django.DjangoModelFactory): title = factory.Sequence("name {}".format) lead = factory.SubFactory(StaffFactory) value = decimal.Decimal("100") - proposed_start = factory.LazyFunction(timezone.now) - proposed_end = factory.LazyFunction(timezone.now) + proposed_start = factory.LazyFunction(timezone.now().date) + proposed_end = factory.LazyFunction(timezone.now().date) is_locked = False From a10bffd47b0d8570f931b932af3c6917bcae65b6 Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Mon, 21 Apr 2025 22:13:51 +0200 Subject: [PATCH 11/12] Lint fixes. --- hypha/apply/projects/reports/tests/test_models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hypha/apply/projects/reports/tests/test_models.py b/hypha/apply/projects/reports/tests/test_models.py index b34c8ae7a1..bead1fee5c 100644 --- a/hypha/apply/projects/reports/tests/test_models.py +++ b/hypha/apply/projects/reports/tests/test_models.py @@ -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.proposed_start - 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 From a4a009e39e728ef92ac70ec7f311712c5d379606 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Mon, 21 Apr 2025 17:49:43 -0400 Subject: [PATCH 12/12] Small bug fix, added form validation, made start date info field more informative for when `PROJECTS_START_AFTER_CONTRACTING` is enabled --- hypha/apply/projects/forms/project.py | 17 +++++++++++++++++ hypha/apply/projects/models/project.py | 2 +- .../partials/project_information.html | 13 ++++++++++--- .../apply/projects/templatetags/project_tags.py | 9 +++++++++ hypha/apply/projects/views/project.py | 1 + 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py index 4c9913f64e..431c68759c 100644 --- a/hypha/apply/projects/forms/project.py +++ b/hypha/apply/projects/forms/project.py @@ -13,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 @@ -475,5 +476,21 @@ 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() diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index b8b3fa65d9..5572a793c0 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -358,7 +358,7 @@ def end_date(self): # 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() diff --git a/hypha/apply/projects/templates/application_projects/partials/project_information.html b/hypha/apply/projects/templates/application_projects/partials/project_information.html index 2c32d61ae1..71736a4060 100644 --- a/hypha/apply/projects/templates/application_projects/partials/project_information.html +++ b/hypha/apply/projects/templates/application_projects/partials/project_information.html @@ -1,4 +1,4 @@ -{% load i18n heroicons %} +{% load i18n heroicons project_tags %}
{% trans "Contractor" %}
@@ -15,9 +15,10 @@
{% trans "E-mail" %}
+ {% show_start_date object as show_start %}
{% trans "Start date" %} - {% if request.user.is_apply_staff %} + {% if request.user.is_apply_staff and show_start %} {% endif %}
-

{{ object.proposed_start|date:"SHORT_DATE_FORMAT"|default:"-" }}

+

+ {% if show_start %} + {{ object.proposed_start|date:"SHORT_DATE_FORMAT"|default:"-" }} + {% else %} + {% trans "Awaiting contract finalization..." %} + {% endif %} +

diff --git a/hypha/apply/projects/templatetags/project_tags.py b/hypha/apply/projects/templatetags/project_tags.py index 5c4d66093c..b8ea6e9431 100644 --- a/hypha/apply/projects/templatetags/project_tags.py +++ b/hypha/apply/projects/templatetags/project_tags.py @@ -341,3 +341,12 @@ def display_project_status(project, user): if user.is_apply_staff or user.is_contracting or user.is_finance: return project.status_display return get_project_public_status(project_status=project.status) + + +@register.simple_tag +def show_start_date(project) -> bool: + return not settings.PROJECTS_START_AFTER_CONTRACTING or project.status in [ + INVOICING_AND_REPORTING, + CLOSING, + COMPLETE, + ] diff --git a/hypha/apply/projects/views/project.py b/hypha/apply/projects/views/project.py index 2de5ff0aaf..46b0f443ad 100644 --- a/hypha/apply/projects/views/project.py +++ b/hypha/apply/projects/views/project.py @@ -741,6 +741,7 @@ def post(self, *args, **kwargs): if settings.PROJECTS_START_AFTER_CONTRACTING: self.project.proposed_start = datetime.date.today() + self.project.save() messenger( MESSAGES.PROJECT_TRANSITION,