diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 749a3a4340..6f55d46e52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/uv-pre-commit - rev: "0.6.4" + rev: "0.6.14" hooks: - id: uv-export name: uv-export requirements/prod.txt @@ -36,7 +36,7 @@ repos: ] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.9.10" + rev: "v0.11.6" hooks: # Run the linter. - id: ruff @@ -62,7 +62,7 @@ repos: types_or: [html, css] - repo: https://github.com/biomejs/pre-commit - rev: "v0.6.1" + rev: "v2.0.0-beta.1" hooks: - id: biome-check additional_dependencies: ["@biomejs/biome@1.9.4"] @@ -77,6 +77,6 @@ repos: - stylelint@16.11.0 - stylelint-config-standard-scss@13.1.0 - repo: https://github.com/gitleaks/gitleaks - rev: v8.24.0 + rev: v8.24.3 hooks: - id: gitleaks diff --git a/Makefile b/Makefile index aef596106e..54c28a5e85 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,15 @@ JS_ESM_DIR = ./hypha/static_src/javascript/esm PIP := $(shell (command -v uv > /dev/null 2>&1 && echo "uv pip") || (command -v pip > /dev/null 2>&1 && echo "pip")) UV_RUN := $(shell (command -v uv > /dev/null 2>&1 && echo "uv run ") || echo "") + +.PHONY: autoupdate +autoupdate: ## Update uv, project dependencies and pre-commit hooks + uv self update + uv lock --upgrade + uv run pre-commit autoupdate + npm update + ${MAKE} fmt + .PHONY: help help: ## Show this help menu with a list of available commands and their descriptions @echo "\nSpecify a command. The choices are:\n" diff --git a/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html b/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html index d4bf281c7e..30c5725555 100644 --- a/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html +++ b/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html @@ -70,9 +70,11 @@

{% trans "My active invoices" %}

diff --git a/hypha/apply/funds/tests/test_views.py b/hypha/apply/funds/tests/test_views.py index 71cd87e902..e92eb3eb08 100644 --- a/hypha/apply/funds/tests/test_views.py +++ b/hypha/apply/funds/tests/test_views.py @@ -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", diff --git a/hypha/apply/funds/views/submission_edit.py b/hypha/apply/funds/views/submission_edit.py index 8ec22231c1..60f3ff65c9 100644 --- a/hypha/apply/funds/views/submission_edit.py +++ b/hypha/apply/funds/views/submission_edit.py @@ -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, ) 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 3e9e01a02e..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 @@ -86,16 +89,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 +126,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)): @@ -444,3 +468,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 new file mode 100644 index 0000000000..b9d30c4cef --- /dev/null +++ b/hypha/apply/projects/migrations/0100_alter_project_proposed_end_and_more.py @@ -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", + ), + ), + ] diff --git a/hypha/apply/projects/models/payment.py b/hypha/apply/projects/models/payment.py index f36f8ae689..afe47dc377 100644 --- a/hypha/apply/projects/models/payment.py +++ b/hypha/apply/projects/models/payment.py @@ -1,6 +1,5 @@ import decimal import os -from textwrap import wrap from django.conf import settings from django.core.validators import MinValueValidator @@ -141,7 +140,7 @@ def __str__(self): ) def transition_invoice_to_resubmitted(self): """ - Tranistion invoice to resubmitted status. + Transition invoice to resubmitted status. This method generally gets used on invoice edit. """ pass @@ -154,16 +153,6 @@ def has_changes_requested(self): def status_display(self): return self.get_status_display() - @property - def vendor_document_number(self): - """ - Vendor document number is a required field to create invoices in IntAcct. - - Formatting should be HP###### i.e. HP000001 and so on. - """ - prefix = "HP-" - return prefix + "-".join(wrap(f"{self.id:06}", 3)) - def can_user_delete(self, user): if user.is_applicant or user.is_apply_staff: if self.status in (SUBMITTED): diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index 2e85867299..08f31b44fd 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 @@ -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 @@ -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() @@ -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) @@ -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. @@ -357,19 +347,11 @@ 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 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/views.py b/hypha/apply/projects/reports/views.py index 978c295e9a..62235ef5eb 100644 --- a/hypha/apply/projects/reports/views.py +++ b/hypha/apply/projects/reports/views.py @@ -277,7 +277,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/tables.py b/hypha/apply/projects/tables.py index df44ba4eb4..5d833c56c7 100644 --- a/hypha/apply/projects/tables.py +++ b/hypha/apply/projects/tables.py @@ -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 "" @@ -176,7 +177,7 @@ class Meta: "fund", "reporting", "last_payment_request", - "created_at", + "end_date", ] model = Project template_name = "application_projects/tables/table.html" @@ -192,7 +193,7 @@ class Meta: "lead", "reporting", "last_payment_request", - "created_at", + "end_date", ] model = Project orderable = False @@ -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"} 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..2c32d61ae1 --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/partials/project_information.html @@ -0,0 +1,49 @@ +{% load i18n heroicons %} + +
+
{% trans "Contractor" %}
+

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

+
+ +
+
{% trans "E-mail" %}
+ {% if object.user.email %} + {{ object.user.email }} + {% else %} + - + {% endif %} +
+ +
+
+ {% 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:"-" }}

+
+ +
+
+ {% trans "End 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_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 c6100e08d9..98c4bd4301 100644 --- a/hypha/apply/projects/templates/application_projects/project_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_detail.html @@ -56,20 +56,15 @@

{% trans "Project Information" %}

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

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

-
- -
-
{% trans "E-mail" %}
- {% if object.user.email %} - {{ object.user.email }} - {% else %} - - - {% endif %} -
+
+ + + +
{% user_can_view_invoices object user as can_view_invoices %} @@ -99,9 +94,9 @@
{% trans "E-mail" %}
{% user_next_step_on_project object user request=request as next_step %} {% if next_step %}