Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add invoice date and paid date to invoice and invoices lists and tables #4019

Merged
merged 8 commits into from
Sep 12, 2024
14 changes: 12 additions & 2 deletions hypha/apply/projects/forms/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ def get_invoice_possible_transition_for_user(user, invoice):
class ChangeInvoiceStatusForm(forms.ModelForm):
name_prefix = "change_invoice_status_form"

paid_date = forms.DateField(required=False)

class Meta:
fields = ["status", "comment"]
fields = ["status", "paid_date", "comment"]
model = Invoice

def __init__(self, instance, user, *args, **kwargs):
Expand All @@ -100,7 +102,13 @@ def __init__(self, instance, user, *args, **kwargs):

class InvoiceBaseForm(forms.ModelForm):
class Meta:
fields = ["invoice_number", "invoice_amount", "document", "message_for_pm"]
fields = [
"invoice_number",
"invoice_amount",
"invoice_date",
"document",
"message_for_pm",
]
model = Invoice

def __init__(self, user=None, *args, **kwargs):
Expand All @@ -124,6 +132,7 @@ class CreateInvoiceForm(FileFormMixin, InvoiceBaseForm):
field_order = [
"invoice_number",
"invoice_amount",
"invoice_date",
"document",
"supporting_documents",
"message_for_pm",
Expand All @@ -149,6 +158,7 @@ class EditInvoiceForm(FileFormMixin, InvoiceBaseForm):
field_order = [
"invoice_number",
"invoice_amount",
"invoice_date",
"document",
"supporting_documents",
"message_for_pm",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.16 on 2024-09-11 12:34

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("application_projects", "0085_alter_projectsettings_paf_approval_sequential"),
]

operations = [
migrations.AddField(
model_name="invoice",
name="invoice_date",
field=models.DateField(null=True, verbose_name="Invoice date"),
),
migrations.AddField(
model_name="invoice",
name="paid_date",
field=models.DateField(null=True, verbose_name="Paid date"),
),
]
2 changes: 2 additions & 0 deletions hypha/apply/projects/models/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ class Invoice(models.Model):
null=True,
verbose_name=_("Invoice amount"),
)
invoice_date = models.DateField(null=True, verbose_name=_("Invoice date"))
paid_date = models.DateField(null=True, verbose_name=_("Paid date"))
status = FSMField(default=SUBMITTED, choices=INVOICE_STATUS_CHOICES)
deliverables = ManyToManyField("InvoiceDeliverable", related_name="invoices")
objects = InvoiceQueryset.as_manager()
Expand Down
57 changes: 53 additions & 4 deletions hypha/apply/projects/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,32 +37,71 @@ class BaseInvoiceTable(tables.Table):
},
},
)
project = tables.Column(verbose_name=_("Project Name"))
status = tables.Column(
attrs={"td": {"data-actions": render_invoice_actions, "class": "js-actions"}},
)
requested_at = tables.DateColumn(verbose_name=_("Submitted"))
invoice_date = tables.DateColumn(verbose_name=_("Invoice date"))


class InvoiceDashboardTable(BaseInvoiceTable):
project = tables.Column(verbose_name=_("Project Name"))

class Meta:
fields = [
"requested_at",
"invoice_number",
"status",
"project",
]
model = Invoice
order_by = ["-requested_at"]
template_name = "application_projects/tables/table.html"
attrs = {"class": "invoices-table"}

def render_project(self, value):
text = (textwrap.shorten(value.title, width=30, placeholder="..."),)
return text[0]


class InvoiceDashboardTable(BaseInvoiceTable):
class FinanceInvoiceTable(BaseInvoiceTable):
vendor_name = tables.Column(verbose_name=_("Vendor Name"), empty_values=())
selected = LabeledCheckboxColumn(
accessor=A("pk"),
attrs={
"input": {"class": "js-batch-select"},
"th__input": {"class": "js-batch-select-all"},
},
)

class Meta:
fields = [
"selected",
"invoice_date",
"requested_at",
"vendor_name",
"invoice_number",
"invoice_amount",
"status",
"project",
]
model = Invoice
order_by = ["-requested_at"]
orderable = True
sequence = fields
order_by = ["-requested_at", "invoice_date"]
template_name = "application_projects/tables/table.html"
attrs = {"class": "invoices-table"}
row_attrs = {
"data-record-id": lambda record: record.id,
}

def render_vendor_name(self, record):
if record.project.vendor:
return record.project.vendor
return record.project.user


class InvoiceListTable(BaseInvoiceTable):
project = tables.Column(verbose_name=_("Project Name"))
fund = tables.Column(verbose_name=_("Fund"), accessor="project__submission__page")
lead = tables.Column(verbose_name=_("Lead"), accessor="project__lead")

Expand All @@ -81,8 +120,13 @@ class Meta:
template_name = "application_projects/tables/table.html"
attrs = {"class": "invoices-table"}

def render_project(self, value):
text = (textwrap.shorten(value.title, width=30, placeholder="..."),)
return text[0]


class AdminInvoiceListTable(BaseInvoiceTable):
project = tables.Column(verbose_name=_("Project Name"))
selected = LabeledCheckboxColumn(
accessor=A("pk"),
attrs={
Expand All @@ -94,6 +138,7 @@ class AdminInvoiceListTable(BaseInvoiceTable):
class Meta:
fields = [
"selected",
"invoice_date",
"requested_at",
"invoice_number",
"status",
Expand All @@ -109,6 +154,10 @@ class Meta:
"data-record-id": lambda record: record.id,
}

def render_project(self, value):
text = (textwrap.shorten(value.title, width=30, placeholder="..."),)
return text[0]


class BaseProjectsTable(tables.Table):
title = tables.LinkColumn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<thead>
<tr>
<th class="data-block__table-date">{% trans "Date submitted" %}</th>
<th class="min-w-[180px] w-[15%]">{% trans "Invoice date" %}</th>
<th class="data-block__table-amount">{% trans "Invoice No." %}</th>
<th class="data-block__table-status">{% trans "Status" %}</th>
<th class="data-block__table-update"></th>
Expand All @@ -26,9 +27,10 @@
{% for invoice in object.invoices.not_rejected %}
{% display_invoice_status_for_user user invoice as invoice_status %}
<tr>
<td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Date submitted" %}: </span>{{ invoice.requested_at.date }}</td>
<td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Invoice number" %}: </span>{{ invoice.invoice_number }}</td>
<td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Status" %}: </span>{{ invoice_status }}</td>
<td class="py-4 px-2"><span class="data-block__mobile-label">{% trans "Date submitted" %}: </span>{{ invoice.requested_at.date }}</td>
<td class="py-4 px-2"><span class="data-block__mobile-label">{% trans "Invoice date" %}: </span>{% if invoice.invoice_date %}{{ invoice.invoice_date }}{% else %} {{ invoice.requested_at.date }} {% endif %}</td>
<td class="py-4 px-2"><span class="data-block__mobile-label">{% trans "Invoice number" %}: </span>{{ invoice.invoice_number }}</td>
<td class="py-4 px-2"><span class="data-block__mobile-label">{% trans "Status" %}: </span>{{ invoice_status }}</td>
<td class="flex flex-wrap justify-center py-4 px-0 gap-2 xl:flex-nowrap">
<a class="data-block__action-icon-link" href="{{ invoice.get_absolute_url }}" >
{% heroicon_micro "eye" aria_hidden=true class="me-1" %}
Expand Down Expand Up @@ -68,6 +70,39 @@ <h4 class="modal__project-header-bar">{% trans "Update Invoice status" %}</h4>
{% trans "Update Status" as update %}
{% include 'funds/includes/delegated_form_base.html' with form=invoice_form value=update action=invoice.get_absolute_url form_id=invoice_form_id %}
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const invoice_form = document.querySelector('[id^={{ invoice_form_id }}')
const invoice_status = invoice_form.querySelector('#id_status');
const paid_field = invoice_form.querySelector('.id_paid_date');
var paid_date = invoice_form.querySelector('#id_paid_date');

function updatePaidDate(){
if (invoice_status.value === 'paid') {
paid_field.style.display = 'block';
if (!paid_date.value) {
// Get today's date
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are zero-based
const day = String(now.getDate()).padStart(2, '0');

// Format the date as YYYY-MM-DD
const today = `${year}-${month}-${day}`;

paid_date.value = today;
}
} else {
paid_field.style.display = 'none';
paid_date.value = '';
}
}

updatePaidDate();

invoice_status.onchange = updatePaidDate;
});
</script>
{% endif %}
</td>
</tr>
Expand All @@ -87,6 +122,7 @@ <h4 class="modal__project-header-bar">{% trans "Update Invoice status" %}</h4>
<thead>
<tr>
<th class="data-block__table-date">{% trans "Date submitted" %}</th>
<th class="min-w-[180px] w-[15%]">{% trans "Invoice date" %}</th>
<th class="data-block__table-amount">{% trans "Invoice number" %}</th>
<th class="data-block__table-status">{% trans "Status" %}</th>
<th class="data-block__table-update"></th>
Expand All @@ -96,9 +132,10 @@ <h4 class="modal__project-header-bar">{% trans "Update Invoice status" %}</h4>
{% for invoice in object.invoices.rejected %}
{% display_invoice_status_for_user user invoice as invoice_status %}
<tr>
<td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Date submitted" %}: </span>{{ invoice.requested_at.date }}</td>
<td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Invoice number" %}: </span>{{ invoice.invoice_number }}</td>
<td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Status" %}: </span>{{ invoice_status }}</td>
<td class="py-4 px-2"><span class="data-block__mobile-label">{% trans "Date submitted" %}: </span>{{ invoice.requested_at.date }}</td>
<td class="py-4 px-2"><span class="data-block__mobile-label">{% trans "Invoice date" %}: </span>{% if invoice.invoice_date %}{{ invoice.invoice_date }}{% else %} {{ invoice.requested_at.date }} {% endif %}</td>
<td class="py-4 px-2"><span class="data-block__mobile-label">{% trans "Invoice number" %}: </span>{{ invoice.invoice_number }}</td>
<td class="py-4 px-2"><span class="data-block__mobile-label">{% trans "Status" %}: </span>{{ invoice_status }}</td>
<td class="flex justify-end py-4 px-0">
<a class="data-block__action-icon-link" href="{{ invoice.get_absolute_url }}" >
{% heroicon_mini "eye" size=16 aria_hidden=true class="me-1" %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,37 @@ <h4 class="modal__project-header-bar">{% trans "Update Invoice status" %}</h4>
<script src="{% static 'js/jquery.fancybox.min.js' %}"></script>
<script src="{% static 'js/fancybox-global.js' %}"></script>
<script src="{% static 'js/deliverables.js' %}"></script>

<script>
document.addEventListener("DOMContentLoaded", function () {
const invoice_status = document.querySelector('[id^=change_invoice_status').querySelector('#id_status');
const paid_field = document.querySelector('.id_paid_date');
var paid_date = document.querySelector('#id_paid_date');

function updatePaidDate(){
if (invoice_status.value === 'paid') {
paid_field.style.display = 'block';
if (!paid_date.value) {
// Get today's date
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are zero-based
const day = String(now.getDate()).padStart(2, '0');

// Format the date as DD-MM-YYYY
const today = `${year}-${month}-${day}`;

paid_date.value = today;
}
} else {
paid_field.style.display = 'none';
paid_date.value = '';
}
}

updatePaidDate();

invoice_status.onchange = updatePaidDate;
});
</script>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{% block body_class %}bg-light-grey{% endblock %}
{% block content %}
{% display_invoice_status_for_user user object as invoice_status %}
{% can_show_paid_date invoice as show_paid_date %}

{% adminbar %}
{% slot back_link %}
Expand All @@ -20,6 +21,10 @@
<div class="wrapper--sidebar--inner">
<div class="card card--solid">
<p class="card__text"><b>{% trans "Invoice number" %}:</b> {{ object.invoice_number }}</p>
<p class="card__text"><b>{% trans "Invoice date" %}:</b> {% if invoice.invoice_date %}{{ invoice.invoice_date }}{% else %} {{ invoice.requested_at.date }} {% endif %}</p>
{% if show_paid_date %}
<p class="card__text"><b>{% trans "Paid date" %}:</b> {{ invoice.paid_date }}</p>
{% endif %}
{% is_vendor_setup request as show_vendor_information %}
<p class="card__text"><b>{% trans "Vendor" %}:</b>
{% if show_vendor_information %}{{ object.project.vendor.name }}{% else %}{{ object.project.user }}{% endif %}</p>
Expand Down
8 changes: 8 additions & 0 deletions hypha/apply/projects/templatetags/invoice_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
INVOICE_STATUS_BG_COLORS,
INVOICE_STATUS_FG_COLORS,
)
from hypha.apply.projects.models.payment import PAID
from hypha.apply.projects.models.project import (
CLOSING,
COMPLETE,
Expand All @@ -30,6 +31,13 @@ def can_change_status(invoice, user):
return invoice.can_user_change_status(user)


@register.simple_tag
def can_show_paid_date(invoice):
if invoice.status == PAID and invoice.paid_date:
return True
return False


@register.simple_tag
def can_delete(invoice, user):
return invoice.can_user_delete(user)
Expand Down
2 changes: 2 additions & 0 deletions hypha/apply/projects/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import decimal

import factory
Expand Down Expand Up @@ -177,6 +178,7 @@ class Meta:
class InvoiceFactory(factory.django.DjangoModelFactory):
invoice_number = factory.Faker("name")
invoice_amount = decimal.Decimal("10")
invoice_date = factory.LazyFunction(datetime.date.today)
project = factory.SubFactory(ProjectFactory)
by = factory.SubFactory(UserFactory)
document = factory.django.FileField()
Expand Down
Loading
Loading