-
Notifications
You must be signed in to change notification settings - Fork 1
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
Invoice Form and Payments #387
Changes from 19 commits
c124b5c
241925c
2b6fb20
2cafbf1
7f00b83
090c178
ad6fb09
72439a9
a9cafd2
5d5ebf3
355a0df
43cec93
bcf603c
8a53dc1
93497ca
6e64326
2c72601
4aeccca
da288f2
65fa403
1ea6c29
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Generated by Django 4.2.5 on 2024-09-05 08:08 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("opportunity", "0056_payment_organization"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="PaymentInvoice", | ||
fields=[ | ||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), | ||
("amount", models.PositiveIntegerField()), | ||
("date", models.DateField()), | ||
("invoice_number", models.CharField(max_length=50)), | ||
( | ||
"opportunity", | ||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="opportunity.opportunity"), | ||
), | ||
], | ||
options={ | ||
"unique_together": {("opportunity", "invoice_number")}, | ||
}, | ||
), | ||
migrations.AddField( | ||
model_name="payment", | ||
name="invoice", | ||
field=models.OneToOneField( | ||
blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to="opportunity.paymentinvoice" | ||
), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
{% extends "opportunity/base.html" %} | ||
{% load django_tables2 %} | ||
{% load static %} | ||
{% load crispy_forms_tags %} | ||
|
||
{% block title %}{{ request.org }} - Invoices{% endblock title %} | ||
|
||
{% block breadcrumbs_inner %} | ||
{{ block.super }} | ||
<li class="breadcrumb-item"> | ||
<a href="{% url 'opportunity:detail' org_slug=request.org.slug pk=opportunity.pk %}"> | ||
{{ opportunity.name }} | ||
</a> | ||
</li> | ||
<li class="breadcrumb-item active" aria-current="page">Invoices</li> | ||
{% endblock %} | ||
|
||
{% block content %} | ||
<h2>Invoices</h2> | ||
<hr /> | ||
{% if not request.org_membership.is_program_manager %} | ||
<button type="button" class="btn btn-primary mb-2" data-bs-toggle="modal" data-bs-target="#invoiceModal"> | ||
{% translate Add New Invoice %} | ||
</button> | ||
{% endif %} | ||
|
||
<form hx-get="{% url "opportunity:invoice_table" org_slug=request.org.slug pk=opportunity.pk %}" | ||
hx-trigger="load, change, newInvoice from:body" | ||
hx-target="#invoiceTable"> | ||
<div class="input-group mb-3" x-data="{ filter: new URLSearchParams(location.search).get('filter')}"> | ||
<label class="input-group-text" for="filterSelect">Filter</label> | ||
<select class="form-select" id="filterSelect" name="filter" x-model="filter"> | ||
<option selected value="">All</option> | ||
<option value="paid">Paid</option> | ||
<option value="pending">Pending</option> | ||
</select> | ||
</div> | ||
</form> | ||
|
||
{% if request.org_membership.is_program_manager %} | ||
<form x-data="{ | ||
selectAll: false, | ||
selected: [], | ||
toggleSelectAll() { | ||
this.selectAll = !this.selectAll; | ||
const checkboxes = document.querySelectorAll('input[name=pk]'); | ||
const allSelected = []; | ||
checkboxes.forEach(el => { | ||
allSelected.push(el.value); | ||
this.selected = this.selectAll ? allSelected : []; | ||
}); | ||
} | ||
}" | ||
hx-post="{% url "opportunity:invoice_approve" org_slug=request.org.slug pk=opportunity.pk %}" | ||
hx-swap="none" | ||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does this need a csrf token? shouldn't the django form handle this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Django was not able to find csrf token on forms submitted using 'hx-post'. This include the csrf token as a request header to provide the csrf token. |
||
{% endif %} | ||
|
||
<div id="invoiceTable"> | ||
{% include "tables/table_placeholder.html" with num_cols=4 %} | ||
</div> | ||
|
||
{% if request.org_membership.is_program_manager %} | ||
<button type="submit" class="btn btn-primary mt-2">{% translate "Pay" %}</button> | ||
</form> | ||
{% endif %} | ||
{% endblock content %} | ||
|
||
{% if not request.org_membership.is_program_manager %} | ||
{% block modal %} | ||
<div class="modal fade" id="invoiceModal" tabindex="-1" role="dialog" aria-labelledby="invoiceModalLabel" aria-hidden="true"> | ||
<div class="modal-dialog" role="document"> | ||
<div class="modal-content"> | ||
<div class="modal-header"> | ||
<h5 class="modal-title" id="invoiceModalLabel">{% translate "Create Invoice" %}</h5> | ||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="invoiceModalClose"></button> | ||
</div> | ||
<form hx-post="{% url "opportunity:invoice_create" org_slug=request.org.slug pk=opportunity.pk %}" | ||
hx-trigger="submit" | ||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' | ||
hx-target="#formFields" | ||
hx-on::after-request="this.reset()"> | ||
<div class="modal-body"> | ||
<div id="formFields">{% crispy form %}</div> | ||
</div> | ||
<div class="modal-footer"> | ||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Close" %}</button> | ||
<button type="submit" class="btn btn-primary">{% translate "Save changes" %}</button> | ||
</div> | ||
</form> | ||
</div> | ||
</div> | ||
</div> | ||
{% endblock modal %} | ||
{% endif %} | ||
|
||
{% block inline_javascript %} | ||
<script> | ||
window.addEventListener('DOMContentLoaded', () => { | ||
const filter = document.querySelector("#filterSelect"); | ||
filter.addEventListener("change", (event) => { | ||
const url = new URL(window.location); | ||
url.searchParams.set("filter", event.target.value) | ||
history.pushState(null, '', url); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this just to share selections via URL? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, this updates the URL to have the filter selected by user. |
||
}); | ||
document.addEventListener('newInvoice', () => document.getElementById('invoiceModalClose').click()); | ||
}); | ||
</script> | ||
{% endblock inline_javascript %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this create duplicates on multiple calls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a filter to the query to only create payment for invoices that don't have a payment.