Test and cover treasury app
@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<h1 class="text-white">{{ title }}</h1>
|
<h1 class="text-white">{{ title }}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle bg-light" style="width: 100%" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle bg-light" style="width: 100%">
|
||||||
<a href="{% url "note:transfer" %}#transfer" class="btn btn-sm btn-outline-primary">
|
<a href="{% url "note:transfer" %}#transfer" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Transfer" %}
|
{% trans "Transfer" %}
|
||||||
</a>
|
</a>
|
||||||
|
@ -24,9 +24,7 @@ class RemittanceAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('remittance_type', 'date', 'comment', 'count', 'amount', 'closed', )
|
list_display = ('remittance_type', 'date', 'comment', 'count', 'amount', 'closed', )
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if not obj:
|
return not obj or (not obj.closed and super().has_change_permission(request, obj))
|
||||||
return True
|
|
||||||
return not obj.closed and super().has_change_permission(request, obj)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SogeCredit, site=admin_site)
|
@admin.register(SogeCredit, site=admin_site)
|
||||||
|
@ -16,7 +16,7 @@ class InvoiceViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/treasury/invoice/
|
then render it on /api/treasury/invoice/
|
||||||
"""
|
"""
|
||||||
queryset = Invoice.objects.all()
|
queryset = Invoice.objects.order_by("id").all()
|
||||||
serializer_class = InvoiceSerializer
|
serializer_class = InvoiceSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['bde', ]
|
filterset_fields = ['bde', ]
|
||||||
@ -28,7 +28,7 @@ class ProductViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/treasury/product/
|
then render it on /api/treasury/product/
|
||||||
"""
|
"""
|
||||||
queryset = Product.objects.all()
|
queryset = Product.objects.order_by("invoice_id", "id").all()
|
||||||
serializer_class = ProductSerializer
|
serializer_class = ProductSerializer
|
||||||
filter_backends = [SearchFilter]
|
filter_backends = [SearchFilter]
|
||||||
search_fields = ['$designation', ]
|
search_fields = ['$designation', ]
|
||||||
@ -40,7 +40,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
|
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
|
||||||
then render it on /api/treasury/remittance_type/
|
then render it on /api/treasury/remittance_type/
|
||||||
"""
|
"""
|
||||||
queryset = RemittanceType.objects
|
queryset = RemittanceType.objects.order_by("id")
|
||||||
serializer_class = RemittanceTypeSerializer
|
serializer_class = RemittanceTypeSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/treasury/remittance/
|
then render it on /api/treasury/remittance/
|
||||||
"""
|
"""
|
||||||
queryset = Remittance.objects
|
queryset = Remittance.objects.order_by("id")
|
||||||
serializer_class = RemittanceSerializer
|
serializer_class = RemittanceSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -60,5 +60,5 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/treasury/soge_credit/
|
then render it on /api/treasury/soge_credit/
|
||||||
"""
|
"""
|
||||||
queryset = SogeCredit.objects
|
queryset = SogeCredit.objects.order_by("id")
|
||||||
serializer_class = SogeCreditSerializer
|
serializer_class = SogeCreditSerializer
|
||||||
|
@ -16,21 +16,15 @@ class InvoiceForm(forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
# If the invoice is locked, it can't be updated.
|
||||||
if self.instance and self.instance.locked:
|
if self.instance and self.instance.locked:
|
||||||
for field_name in self.fields:
|
for field_name in self.fields:
|
||||||
self.cleaned_data[field_name] = getattr(self.instance, field_name)
|
self.cleaned_data[field_name] = getattr(self.instance, field_name)
|
||||||
self.errors.clear()
|
self.errors.clear()
|
||||||
|
self.add_error(None, _('This invoice is locked and can no longer be edited.'))
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
return super().clean()
|
return super().clean()
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
"""
|
|
||||||
If the invoice is locked, don't save it
|
|
||||||
"""
|
|
||||||
if not self.instance.locked:
|
|
||||||
super().save(commit)
|
|
||||||
return self.instance
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Invoice
|
model = Invoice
|
||||||
exclude = ('bde', 'date', 'tex', )
|
exclude = ('bde', 'date', 'tex', )
|
||||||
|
@ -85,7 +85,7 @@ class Invoice(models.Model):
|
|||||||
|
|
||||||
old_invoice = Invoice.objects.filter(id=self.id)
|
old_invoice = Invoice.objects.filter(id=self.id)
|
||||||
if old_invoice.exists():
|
if old_invoice.exists():
|
||||||
if old_invoice.get().locked:
|
if old_invoice.get().locked and not self._force_save:
|
||||||
raise ValidationError(_("This invoice is locked and can no longer be edited."))
|
raise ValidationError(_("This invoice is locked and can no longer be edited."))
|
||||||
|
|
||||||
products = self.products.all()
|
products = self.products.all()
|
||||||
@ -224,7 +224,7 @@ class Remittance(models.Model):
|
|||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||||
# Check if all transactions have the right type.
|
# Check if all transactions have the right type.
|
||||||
if self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
|
if self.transactions.exists() and self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
|
||||||
raise ValidationError("All transactions in a remittance must have the same type")
|
raise ValidationError("All transactions in a remittance must have the same type")
|
||||||
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 752 KiB |
Before Width: | Height: | Size: 664 KiB After Width: | Height: | Size: 664 KiB |
Before Width: | Height: | Size: 414 KiB After Width: | Height: | Size: 414 KiB |
Before Width: | Height: | Size: 375 KiB After Width: | Height: | Size: 375 KiB |
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB |
@ -34,7 +34,7 @@ class InvoiceTable(tables.Table):
|
|||||||
|
|
||||||
delete = tables.LinkColumn(
|
delete = tables.LinkColumn(
|
||||||
'treasury:invoice_delete',
|
'treasury:invoice_delete',
|
||||||
args=[A('pk')],
|
args=[A('id')],
|
||||||
verbose_name=_("delete"),
|
verbose_name=_("delete"),
|
||||||
text=_("Delete"),
|
text=_("Delete"),
|
||||||
attrs={
|
attrs={
|
||||||
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
|
||||||
<a href="#" class="btn btn-sm btn-outline-primary active">
|
<a href="#" class="btn btn-sm btn-outline-primary active">
|
||||||
{% trans "Invoice" %}s
|
{% trans "Invoice" %}s
|
||||||
</a>
|
</a>
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
\parbox[b][\paperheight]{\paperwidth}{%
|
\parbox[b][\paperheight]{\paperwidth}{%
|
||||||
\vfill
|
\vfill
|
||||||
\centering
|
\centering
|
||||||
{\transparent{0.1}\includegraphics[width=\textwidth]{../../static/img/{{ obj.bde }}}}%
|
{\transparent{0.1}\includegraphics[width=\textwidth]{../../apps/treasury/static/img/{{ obj.bde }}}}%
|
||||||
\vfill
|
\vfill
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
|
||||||
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
|
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Invoice" %}s
|
{% trans "Invoice" %}s
|
||||||
</a>
|
</a>
|
||||||
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
|
||||||
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
|
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Invoice" %}s
|
{% trans "Invoice" %}s
|
||||||
</a>
|
</a>
|
||||||
@ -59,9 +59,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
function reloadTable() {
|
function reloadTable() {
|
||||||
let pattern = searchbar_obj.val();
|
let pattern = searchbar_obj.val();
|
||||||
|
|
||||||
if (pattern === old_pattern || pattern === "")
|
|
||||||
return;
|
|
||||||
|
|
||||||
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
|
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
|
||||||
invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table");
|
invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table");
|
||||||
|
|
||||||
|
0
apps/treasury/tests/__init__.py
Normal file
403
apps/treasury/tests/test_treasury.py
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from member.models import Membership, Club
|
||||||
|
from note.models import SpecialTransaction, NoteSpecial, Transaction
|
||||||
|
from treasury.models import Invoice, Product, Remittance, RemittanceType, SogeCredit
|
||||||
|
|
||||||
|
|
||||||
|
class TestInvoices(TestCase):
|
||||||
|
"""
|
||||||
|
Check that invoices can be created and rendered properly.
|
||||||
|
"""
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="admintoto",
|
||||||
|
password="totototo",
|
||||||
|
email="admin@example.com",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
self.invoice = Invoice.objects.create(
|
||||||
|
id=1,
|
||||||
|
object="Object",
|
||||||
|
description="Description",
|
||||||
|
name="Me",
|
||||||
|
address="Earth",
|
||||||
|
acquitted=False,
|
||||||
|
)
|
||||||
|
self.product = Product.objects.create(
|
||||||
|
invoice=self.invoice,
|
||||||
|
designation="Product",
|
||||||
|
quantity=3,
|
||||||
|
amount=3.14,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_admin_page(self):
|
||||||
|
"""
|
||||||
|
Display the invoice admin page.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("admin:index") + "treasury/invoice/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invoices_list(self):
|
||||||
|
"""
|
||||||
|
Display the list of invoices.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invoice_create(self):
|
||||||
|
"""
|
||||||
|
Try to create a new invoice.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_create"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:invoice_create"), data={
|
||||||
|
"id": 42,
|
||||||
|
"object": "Same object",
|
||||||
|
"description": "Longer description",
|
||||||
|
"name": "Me and others",
|
||||||
|
"address": "Alwways earth",
|
||||||
|
"acquitted": True,
|
||||||
|
"products-0-designation": "Designation",
|
||||||
|
"products-0-quantity": 1,
|
||||||
|
"products-0-amount": 42,
|
||||||
|
"products-TOTAL_FORMS": 1,
|
||||||
|
"products-INITIAL_FORMS": 0,
|
||||||
|
"products-MIN_NUM_FORMS": 0,
|
||||||
|
"products-MAX_NUM_FORMS": 1000,
|
||||||
|
})
|
||||||
|
self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200)
|
||||||
|
self.assertTrue(Invoice.objects.filter(object="Same object", id=42).exists())
|
||||||
|
self.assertTrue(Product.objects.filter(designation="Designation", invoice_id=42).exists())
|
||||||
|
self.assertTrue(Invoice.objects.get(id=42).tex)
|
||||||
|
|
||||||
|
def test_invoice_update(self):
|
||||||
|
"""
|
||||||
|
Try to update an invoice.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_update", args=(self.invoice.id,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"object": "Same object",
|
||||||
|
"description": "Longer description",
|
||||||
|
"name": "Me and others",
|
||||||
|
"address": "Always earth",
|
||||||
|
"acquitted": True,
|
||||||
|
"locked": True,
|
||||||
|
"products-0-designation": "Designation",
|
||||||
|
"products-0-quantity": 1,
|
||||||
|
"products-0-amount": 4200,
|
||||||
|
"products-1-designation": "Second designation",
|
||||||
|
"products-1-quantity": 5,
|
||||||
|
"products-1-amount": -1800,
|
||||||
|
"products-TOTAL_FORMS": 2,
|
||||||
|
"products-INITIAL_FORMS": 0,
|
||||||
|
"products-MIN_NUM_FORMS": 0,
|
||||||
|
"products-MAX_NUM_FORMS": 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:invoice_update", args=(self.invoice.id,)), data=data)
|
||||||
|
self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200)
|
||||||
|
self.invoice.refresh_from_db()
|
||||||
|
self.assertTrue(Invoice.objects.filter(pk=1, object="Same object", locked=True).exists())
|
||||||
|
self.assertTrue(Product.objects.filter(designation="Second designation", invoice_id=1).exists())
|
||||||
|
|
||||||
|
# Resend the same data, but the invoice is locked.
|
||||||
|
response = self.client.get(reverse("treasury:invoice_update", args=(self.invoice.id,)))
|
||||||
|
self.assertTrue(response.status_code, 200)
|
||||||
|
response = self.client.post(reverse("treasury:invoice_update", args=(self.invoice.id,)), data=data)
|
||||||
|
self.assertTrue(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_delete_invoice(self):
|
||||||
|
"""
|
||||||
|
Try to delete an invoice.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_delete", args=(self.invoice.id,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Can't delete a locked invoice
|
||||||
|
self.invoice.locked = True
|
||||||
|
self.invoice.save()
|
||||||
|
response = self.client.delete(reverse("treasury:invoice_delete", args=(self.invoice.id,)))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertTrue(Invoice.objects.filter(pk=self.invoice.id).exists())
|
||||||
|
|
||||||
|
# Unlock invoice and truly delete it.
|
||||||
|
self.invoice.locked = False
|
||||||
|
self.invoice._force_save = True
|
||||||
|
self.invoice.save()
|
||||||
|
response = self.client.delete(reverse("treasury:invoice_delete", args=(self.invoice.id,)))
|
||||||
|
self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200)
|
||||||
|
self.assertFalse(Invoice.objects.filter(pk=self.invoice.id).exists())
|
||||||
|
|
||||||
|
def test_invoice_render_pdf(self):
|
||||||
|
"""
|
||||||
|
Generate the PDF file of an invoice.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_render", args=(self.invoice.id,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invoice_api(self):
|
||||||
|
"""
|
||||||
|
Load some API pages
|
||||||
|
"""
|
||||||
|
response = self.client.get("/api/treasury/invoice/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get("/api/treasury/product/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRemittances(TestCase):
|
||||||
|
"""
|
||||||
|
Create some credits and close remittances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixtures = ('initial',)
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="admintoto",
|
||||||
|
password="totototo",
|
||||||
|
email="admin@example.com",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
self.credit = SpecialTransaction.objects.create(
|
||||||
|
source=NoteSpecial.objects.get(special_type="Chèque"),
|
||||||
|
destination=self.user.note,
|
||||||
|
amount=4200,
|
||||||
|
reason="Credit",
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.second_credit = SpecialTransaction.objects.create(
|
||||||
|
source=self.user.note,
|
||||||
|
destination=NoteSpecial.objects.get(special_type="Chèque"),
|
||||||
|
amount=424200,
|
||||||
|
reason="Second credit",
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.remittance = Remittance.objects.create(
|
||||||
|
remittance_type=RemittanceType.objects.get(),
|
||||||
|
comment="Test remittance",
|
||||||
|
closed=False,
|
||||||
|
)
|
||||||
|
self.credit.specialtransactionproxy.remittance = self.remittance
|
||||||
|
self.credit.specialtransactionproxy.save()
|
||||||
|
|
||||||
|
def test_admin_page(self):
|
||||||
|
"""
|
||||||
|
Load the admin page.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("admin:index") + "treasury/remittance/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_remittances_list(self):
|
||||||
|
"""
|
||||||
|
Display the remittance list.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:remittance_list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_remittance_create(self):
|
||||||
|
"""
|
||||||
|
Create a new Remittance.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:remittance_create"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:remittance_create"), data=dict(
|
||||||
|
remittance_type=RemittanceType.objects.get().pk,
|
||||||
|
comment="Created remittance",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
self.assertTrue(Remittance.objects.filter(comment="Created remittance").exists())
|
||||||
|
|
||||||
|
def test_remittance_update(self):
|
||||||
|
"""
|
||||||
|
Update an existing remittance.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:remittance_update", args=(self.remittance.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:remittance_update", args=(self.remittance.pk,)), data=dict(
|
||||||
|
comment="Updated remittance",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
self.assertTrue(Remittance.objects.filter(comment="Updated remittance").exists())
|
||||||
|
|
||||||
|
def test_remittance_close(self):
|
||||||
|
"""
|
||||||
|
Try to close an open remittance.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:remittance_update", args=(self.remittance.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:remittance_update", args=(self.remittance.pk,)), data=dict(
|
||||||
|
comment="Closed remittance",
|
||||||
|
close=True,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
self.assertTrue(Remittance.objects.filter(comment="Closed remittance", closed=True).exists())
|
||||||
|
|
||||||
|
def test_remittance_link_transaction(self):
|
||||||
|
"""
|
||||||
|
Link a transaction to an open remittance.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:link_transaction", args=(self.credit.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:link_transaction", args=(self.credit.pk,)), data=dict(
|
||||||
|
remittance=self.remittance.pk,
|
||||||
|
last_name="Last Name",
|
||||||
|
first_name="First Name",
|
||||||
|
bank="Bank",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
self.credit.refresh_from_db()
|
||||||
|
self.assertEqual(self.credit.last_name, "Last Name")
|
||||||
|
self.assertEqual(self.remittance.transactions.count(), 1)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("treasury:unlink_transaction", args=(self.credit.pk,)))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
|
||||||
|
def test_invoice_api(self):
|
||||||
|
"""
|
||||||
|
Load some API pages
|
||||||
|
"""
|
||||||
|
response = self.client.get("/api/treasury/remittance_type/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get("/api/treasury/remittance/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSogeCredits(TestCase):
|
||||||
|
"""
|
||||||
|
Check that credits from the Société générale are working correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixtures = ('initial',)
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="admintoto",
|
||||||
|
password="totototo",
|
||||||
|
email="admin@example.com",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
self.kfet = Club.objects.get(name="Kfet")
|
||||||
|
self.bde = self.kfet.parent_club
|
||||||
|
|
||||||
|
self.kfet_membership = Membership(
|
||||||
|
user=self.user,
|
||||||
|
club=self.kfet,
|
||||||
|
)
|
||||||
|
self.kfet_membership._force_renew_parent = True
|
||||||
|
self.kfet_membership._soge = True
|
||||||
|
self.kfet_membership.save()
|
||||||
|
|
||||||
|
def test_admin_page(self):
|
||||||
|
"""
|
||||||
|
Render the admin page.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("admin:index") + "treasury/sogecredit/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_sogecredit_list(self):
|
||||||
|
"""
|
||||||
|
Display the list of all credits.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:soge_credits"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("treasury:soge_credits") + "?search=toto&valid=")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_validate_soge_credit(self):
|
||||||
|
"""
|
||||||
|
Try to validate a credit.
|
||||||
|
"""
|
||||||
|
soge_credit = SogeCredit.objects.get(user=self.user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(
|
||||||
|
validate=True,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), 302, 200)
|
||||||
|
soge_credit.refresh_from_db()
|
||||||
|
self.assertTrue(soge_credit.valid)
|
||||||
|
self.user.note.refresh_from_db()
|
||||||
|
self.assertEqual(self.user.note.balance, 0)
|
||||||
|
self.assertEqual(
|
||||||
|
Transaction.objects.filter(Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
|
||||||
|
self.assertTrue(self.user.profile.soge)
|
||||||
|
|
||||||
|
def test_delete_soge_credit(self):
|
||||||
|
"""
|
||||||
|
Try to invalidate a credit.
|
||||||
|
"""
|
||||||
|
soge_credit = SogeCredit.objects.get(user=self.user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True))
|
||||||
|
raise AssertionError("It is not possible to delete the soge credit until the note is not credited.")
|
||||||
|
except ValidationError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
SpecialTransaction.objects.create(
|
||||||
|
source=NoteSpecial.objects.get(special_type="Carte bancaire"),
|
||||||
|
destination=self.user.note,
|
||||||
|
amount=self.bde.membership_fee_paid + self.kfet.membership_fee_paid,
|
||||||
|
quantity=1,
|
||||||
|
reason="Registration is not complete, pliz pay",
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)),
|
||||||
|
data=dict(delete=True))
|
||||||
|
# 403 because no SogeCredit exists anymore, then a PermissionDenied is raised
|
||||||
|
self.assertRedirects(response, reverse("treasury:soge_credits"), 302, 403)
|
||||||
|
self.assertFalse(SogeCredit.objects.filter(pk=soge_credit.pk))
|
||||||
|
self.user.note.refresh_from_db()
|
||||||
|
self.assertEqual(self.user.note.balance, 0)
|
||||||
|
self.assertEqual(
|
||||||
|
Transaction.objects.filter(Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
|
||||||
|
self.assertFalse(self.user.profile.soge)
|
||||||
|
|
||||||
|
def test_invoice_api(self):
|
||||||
|
"""
|
||||||
|
Load some API pages
|
||||||
|
"""
|
||||||
|
response = self.client.get("/api/treasury/soge_credit/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
@ -60,6 +60,11 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_form(self, form_class=None):
|
||||||
|
form = super().get_form(form_class)
|
||||||
|
del form.fields["locked"]
|
||||||
|
return form
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
@ -134,6 +139,11 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_form(self, form_class=None):
|
||||||
|
form = super().get_form(form_class)
|
||||||
|
del form.fields["id"]
|
||||||
|
return form
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
@ -165,6 +175,11 @@ class InvoiceDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
|
|||||||
model = Invoice
|
model = Invoice
|
||||||
extra_context = {"title": _("Delete invoice")}
|
extra_context = {"title": _("Delete invoice")}
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
if self.get_object().locked:
|
||||||
|
raise PermissionDenied(_("This invoice is locked and can't be deleted."))
|
||||||
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('treasury:invoice_list')
|
return reverse_lazy('treasury:invoice_list')
|
||||||
|
|
||||||
@ -387,7 +402,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
|
|||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
if not self.get_queryset().exists():
|
if not super().get_queryset().exists():
|
||||||
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ $(document).ready(function() {
|
|||||||
let source = $("#source_note");
|
let source = $("#source_note");
|
||||||
let dest = $("#dest_note");
|
let dest = $("#dest_note");
|
||||||
|
|
||||||
$("#type_transfer").click(function() {
|
$("#type_transfer").change(function() {
|
||||||
if (LOCK)
|
if (LOCK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ $(document).ready(function() {
|
|||||||
location.hash = "transfer";
|
location.hash = "transfer";
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#type_credit").click(function() {
|
$("#type_credit").change(function() {
|
||||||
if (LOCK)
|
if (LOCK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ $(document).ready(function() {
|
|||||||
location.hash = "credit";
|
location.hash = "credit";
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#type_debit").click(function() {
|
$("#type_debit").change(function() {
|
||||||
if (LOCK)
|
if (LOCK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|