facturation: pouvoir annuler un règlement (#88592) #176
|
@ -17,7 +17,9 @@
|
|||
|
||||
import django_filters
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from gadjo.forms.widgets import MultiSelectWidget
|
||||
|
||||
|
@ -36,6 +38,7 @@ from lingo.invoicing.models import (
|
|||
JournalLine,
|
||||
Payer,
|
||||
Payment,
|
||||
PaymentCancellationReason,
|
||||
PaymentType,
|
||||
Refund,
|
||||
Regie,
|
||||
|
@ -769,6 +772,16 @@ class RegiePaymentFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet
|
|||
empty_label=_('all'),
|
||||
method='filter_event',
|
||||
)
|
||||
cancelled = django_filters.ChoiceFilter(
|
||||
label=_('Cancelled'),
|
||||
widget=forms.RadioSelect,
|
||||
empty_label=_('all'),
|
||||
choices=[
|
||||
('yes', _('Yes')),
|
||||
('no', _('No')),
|
||||
],
|
||||
method='filter_cancelled',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Payment
|
||||
|
@ -804,6 +817,33 @@ class RegiePaymentFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet
|
|||
lines = InvoiceLine.objects.filter(event_slug=value)
|
||||
return queryset.filter(pk__in=InvoiceLinePayment.objects.filter(line__in=lines).values('payment'))
|
||||
|
||||
def filter_cancelled(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
if value == 'yes':
|
||||
return queryset.filter(cancelled_at__isnull=False)
|
||||
return queryset.filter(cancelled_at__isnull=True)
|
||||
|
||||
|
||||
class RegiePaymentCancelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Payment
|
||||
fields = ['cancellation_reason', 'cancellation_description']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['cancellation_reason'].queryset = PaymentCancellationReason.objects.filter(disabled=False)
|
||||
|
||||
def save(self):
|
||||
super().save(commit=False)
|
||||
self.instance.cancelled_at = now()
|
||||
self.instance.cancelled_by = self.request.user
|
||||
with transaction.atomic():
|
||||
self.instance.save()
|
||||
self.instance.invoicelinepayment_set.all().delete()
|
||||
return self.instance
|
||||
|
||||
|
||||
class RegieCreditFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet):
|
||||
number = django_filters.CharFilter(
|
||||
|
@ -1062,3 +1102,19 @@ class PayerMappingForm(forms.ModelForm):
|
|||
self.instance.user_fields_mapping = {k: self.cleaned_data[k] for k, v in self.instance.user_variables}
|
||||
self.instance.save()
|
||||
return self.instance
|
||||
|
||||
|
||||
class PaymentCancellationReasonForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = PaymentCancellationReason
|
||||
fields = ['label', 'slug', 'disabled']
|
||||
|
||||
def clean_slug(self):
|
||||
slug = self.cleaned_data['slug']
|
||||
|
||||
if PaymentCancellationReason.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
|
||||
raise forms.ValidationError(
|
||||
_('Another payment cancellation reason exists with the same identifier.')
|
||||
)
|
||||
|
||||
return slug
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('invoicing', '0083_credit_activity_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='payment',
|
||||
name='cancellation_description',
|
||||
field=models.TextField(blank=True, verbose_name='Description'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='payment',
|
||||
name='cancelled_at',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='payment',
|
||||
name='cancelled_by',
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentCancellationReason',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('label', models.CharField(max_length=150, verbose_name='Label')),
|
||||
('slug', models.SlugField(max_length=160, unique=True, verbose_name='Identifier')),
|
||||
('disabled', models.BooleanField(default=False, verbose_name='Disabled')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['label'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='payment',
|
||||
name='cancellation_reason',
|
||||
field=models.ForeignKey(
|
||||
default='',
|
||||
verbose_name='Cancellation reason',
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to='invoicing.paymentcancellationreason',
|
||||
null=True,
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -30,7 +30,7 @@ from django.core import validators
|
|||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import connection, models, transaction
|
||||
from django.template import RequestContext, Template, TemplateSyntaxError, VariableDoesNotExist
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.template.defaultfilters import floatformat, linebreaksbr
|
||||
from django.template.loader import get_template
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.formats import date_format
|
||||
|
@ -1314,6 +1314,27 @@ class PaymentType(models.Model):
|
|||
return created, payment_type
|
||||
|
||||
|
||||
class PaymentCancellationReason(models.Model):
|
||||
label = models.CharField(_('Label'), max_length=150)
|
||||
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
|
||||
disabled = models.BooleanField(_('Disabled'), default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ['label']
|
||||
|
||||
pmarillonnet marked this conversation as resolved
Outdated
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = generate_slug(self)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def base_slug(self):
|
||||
return slugify(self.label)
|
||||
|
||||
|
||||
PAYMENT_INFO = [
|
||||
('check_issuer', _('Issuer')),
|
||||
('check_bank', _('Bank/Organism')),
|
||||
|
@ -1338,6 +1359,13 @@ class Payment(models.Model):
|
|||
payer_last_name = models.CharField(max_length=250)
|
||||
payer_address = models.TextField()
|
||||
|
||||
cancelled_at = models.DateTimeField(null=True)
|
||||
cancelled_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
|
||||
cancellation_reason = models.ForeignKey(
|
||||
PaymentCancellationReason, verbose_name=_('Cancellation reason'), on_delete=models.PROTECT, null=True
|
||||
)
|
||||
cancellation_description = models.TextField(_('Description'), blank=True)
|
||||
|
||||
order_date = models.DateTimeField(null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
@ -1430,6 +1458,18 @@ class Payment(models.Model):
|
|||
result.append((label, self.payment_info[key]))
|
||||
return result
|
||||
|
||||
def get_cancellation_info(self):
|
||||
result = []
|
||||
if not self.cancelled_at:
|
||||
return result
|
||||
result.append((_('Cancelled on'), self.cancelled_at.strftime('%d/%m/%Y %H:%M')))
|
||||
if self.cancelled_by:
|
||||
pmarillonnet marked this conversation as resolved
Outdated
pmarillonnet
commented
Pareil ici, plutôt Pareil ici, plutôt `'Cancelled on'`.
lguerin
commented
fixup pour ça fixup pour ça
|
||||
result.append((_('Cancelled by'), self.cancelled_by))
|
||||
result.append((_('Reason'), self.cancellation_reason))
|
||||
if self.cancellation_description:
|
||||
result.append((_('Description'), linebreaksbr(self.cancellation_description)))
|
||||
return result
|
||||
|
||||
def get_invoice_payments(self):
|
||||
if hasattr(self, 'prefetched_invoicelinepayments'):
|
||||
invoice_line_payments = self.prefetched_invoicelinepayments
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
{% extends "lingo/invoicing/manager_regie_list.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page-title-extra-label %}{% trans "Cancellation reasons" %}{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
<a href="{% url 'lingo-manager-homepage' %}">{% trans "Payments" context 'lingo title' %}</a>
|
||||
<a href="{% url 'lingo-manager-invoicing-regie-list' %}">{% trans "Regies" %}</a>
|
||||
<a href="{% url 'lingo-manager-invoicing-cancellation-reason-list' %}">{% trans "Cancellation reasons" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Cancellation reasons" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="pk-tabs">
|
||||
<div class="pk-tabs--tab-list" role="tablist">
|
||||
<button aria-controls="panel-payment" aria-selected="true" id="tab-payment" role="tab" tabindex="0">{% trans "Payments" %}</button>
|
||||
lguerin
commented
dans l'idée d'avoir aussi des motifs d'annulation pour les factures, j'ai fait un onglet "payments" pour avoir un onglet "invoices" dans un futur ticket dans l'idée d'avoir aussi des motifs d'annulation pour les factures, j'ai fait un onglet "payments" pour avoir un onglet "invoices" dans un futur ticket
|
||||
</div>
|
||||
<div class="pk-tabs--container">
|
||||
|
||||
<div aria-labelledby="tab-payment" id="panel-payment" role="tabpanel" tabindex="0">
|
||||
<ul class="objects-list single-links">
|
||||
{% for reason in payment_reason_list %}
|
||||
<li>
|
||||
<a href="{% url 'lingo-manager-invoicing-payment-cancellation-reason-edit' reason.pk %}">{{ reason }}{% if reason.disabled %}<span class="extra-info"> ({% trans "disabled" %})</span>{% endif %}</a>
|
||||
{% if not reason.payment_set.exists %}
|
||||
<a class="delete" rel="popup" href="{% url 'lingo-manager-invoicing-payment-cancellation-reason-delete' reason.pk %}">{% trans "delete"%}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="panel--buttons">
|
||||
<a class="pk-button" href="{% url 'lingo-manager-invoicing-payment-cancellation-reason-add' %}" rel="popup">{% trans "New payment cancellation reason" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "lingo/invoicing/manager_payment_list.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'lingo-manager-invoicing-regie-payment-cancel' regie.pk object.pk %}">{% trans "Cancel payment" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Cancel payment" %} - {{ object.formatted_number }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form|with_template }}
|
||||
<div class="buttons">
|
||||
<button>{% trans "Submit" %}</button>
|
||||
<a class="cancel" href="{% url 'lingo-manager-invoicing-regie-payment-list' regie.pk %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "lingo/invoicing/manager_cancellation_reason_list.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
{% if object.pk %}
|
||||
<a href="{% url 'lingo-manager-invoicing-payment-cancellation-reason-edit' object.pk %}">{% trans "Edit" %}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'lingo-manager-invoicing-payment-cancellation-reason-add' %}">{% trans "New payment cancellation reason" %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
{% if object.pk %}
|
||||
<h2>{% trans "Edit payment cancellation reason" %} - {{ reason }}</h2>
|
||||
{% else %}
|
||||
<h2>{% trans "New paymen cancellation reason" %}</h2>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form|with_template }}
|
||||
<div class="buttons">
|
||||
<button>{% trans "Submit" %}</button>
|
||||
<a class="cancel" href="{% url 'lingo-manager-invoicing-cancellation-reason-list' %}#open:payment">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -34,39 +34,63 @@
|
|||
<table class="main pk-compact-table invoicing-element-list">
|
||||
{% url 'lingo-manager-invoicing-regie-invoice-list' regie_pk=regie.pk as regie_invoice_list_url %}
|
||||
{% for payment in object_list %}
|
||||
{% with invoice_payments=payment.get_invoice_payments %}
|
||||
<tr class="payment untoggled" data-invoicing-element-id="{{ payment.pk }}">
|
||||
<td colspan="4">
|
||||
{% blocktrans with payment_number=payment.formatted_number cdate=payment.created_at|date:'d/m/Y' payer_id=invoice_payments.0.invoice.payer_external_id payer_name=invoice_payments.0.invoice.payer_name amount=payment.amount payment_type=payment.payment_type %}Payment {{ payment_number }} dated {{ cdate }} from <a href="?payer_external_id={{ payer_id }}">{{ payer_name }}</a>, amount {{ amount }}€ ({{ payment_type }}){% endblocktrans %}
|
||||
- <a href="{% url 'lingo-manager-invoicing-regie-payment-pdf' regie_pk=regie.pk payment_pk=payment.pk %}">{% trans "download" %}</a>
|
||||
</td>
|
||||
<td class="with-togglable">
|
||||
<span class="togglable"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
|
||||
<td>{% trans "Invoice" %}</td>
|
||||
<td class="amount" colspan="2">{% trans "Amount charged" %}</td>
|
||||
<td class="amount" colspan="2">{% trans "Amount assigned" %}</td>
|
||||
</tr>
|
||||
{% for invoice_payment in invoice_payments %}
|
||||
<tr class="line {% if forloop.last %}last-line{% endif %}" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
|
||||
<td class="invoice">
|
||||
<a href="{{ regie_invoice_list_url }}?number={{ invoice_payment.invoice.formatted_number }}">{{ invoice_payment.invoice.formatted_number }}</a>
|
||||
</td>
|
||||
<td class="amount" colspan="2">{% blocktrans with amount=invoice_payment.invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
<td class="amount" colspan="2">{% blocktrans with amount=invoice_payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% for label, value in payment.get_payment_info %}
|
||||
<tr class="payment untoggled" data-invoicing-element-id="{{ payment.pk }}">
|
||||
<td colspan="4">
|
||||
{% if payment.cancelled_at %}
|
||||
<span class="meta meta-success">{% trans "Cancelled" %}</span>
|
||||
{% endif %}
|
||||
{% blocktrans with payment_number=payment.formatted_number cdate=payment.created_at|date:'d/m/Y' payer_id=payment.payer_external_id payer_name=payment.payer_name amount=payment.amount payment_type=payment.payment_type %}Payment {{ payment_number }} dated {{ cdate }} from <a href="?payer_external_id={{ payer_id }}">{{ payer_name }}</a>, amount {{ amount }}€ ({{ payment_type }}){% endblocktrans %}
|
||||
- <a href="{% url 'lingo-manager-invoicing-regie-payment-pdf' regie_pk=regie.pk payment_pk=payment.pk %}">{% trans "download" %}</a>
|
||||
</td>
|
||||
<td class="with-togglable">
|
||||
<span class="togglable"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
|
||||
<td>{% trans "Invoice" %}</td>
|
||||
<td class="amount" colspan="2">{% trans "Amount charged" %}</td>
|
||||
<td class="amount" colspan="2">{% trans "Amount assigned" %}</td>
|
||||
</tr>
|
||||
{% for invoice_payment in payment.get_invoice_payments %}
|
||||
<tr class="line {% if forloop.last %}last-line{% endif %}" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
|
||||
<td class="invoice">
|
||||
<a href="{{ regie_invoice_list_url }}?number={{ invoice_payment.invoice.formatted_number }}">{{ invoice_payment.invoice.formatted_number }}</a>
|
||||
</td>
|
||||
<td class="amount" colspan="2">{% blocktrans with amount=invoice_payment.invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
<td class="amount" colspan="2">{% blocktrans with amount=invoice_payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr class="line" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
|
||||
<td colspan="5" class="no-assignments">
|
||||
{% trans "No assignments for this payment" %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for label, value in payment.get_payment_info %}
|
||||
<tr class="line" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
|
||||
<td colspan="2"></td>
|
||||
<td class="payment-details" colspan="3">
|
||||
<i>{% blocktrans %}{{ label }}:{% endblocktrans %} {{ value }}</i>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not payment.cancelled_at %}
|
||||
<tr class="line last-line" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
|
||||
<td colspan="2"></td>
|
||||
<td class="payment-details" colspan="3">
|
||||
<a href="{% url 'lingo-manager-invoicing-regie-payment-cancel' regie_pk=regie.pk payment_pk=payment.pk %}">{% trans "Cancel payment" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for label, value in payment.get_cancellation_info %}
|
||||
<tr class="line {% if forloop.last %}last-line{% endif %}" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
|
||||
<td colspan="2"></td>
|
||||
<td class="payment-details" colspan="3">
|
||||
<i>{% blocktrans %}{{ label }}:{% endblocktrans %} {{ value }}</i>
|
||||
pmarillonnet marked this conversation as resolved
Outdated
pmarillonnet
commented
Du détail i18n, mais je mettrais tout le Du détail i18n, mais je mettrais tout le `{{ label }}: {{ value }}` dans le bloc internationalisé (genre il y a des langues comme le grec ou bien certaines langues slaves où les deux points n’ont pas cette signification descriptive, et où il faudrait traduire le bloc entier d’une façon différente).
lguerin
commented
Alors il faudrait reprendre les trads de toutes les briques, on a souvent des chaînes qui terminent par ":". Alors il faudrait reprendre les trads de toutes les briques, on a souvent des chaînes qui terminent par ":".
pmarillonnet
commented
Oui je sais bien, je lutte contre ça mais tu as raison, ce bateau a déjà pris les voiles :) Oui je sais bien, je lutte contre ça mais tu as raison, ce bateau a déjà pris les voiles :)
(pour tout te dire il y a même des endroits dans les briques où c’est “internationalisé” en `{{ label }}{% trans ": " %}{{ value }}` 🙃).
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% include "gadjo/pagination.html" %}
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
<h3>{% trans "Navigation" %}</h3>
|
||||
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-appearance-settings' %}">{% trans "Appearance Settings" %}</a>
|
||||
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-payer-list' %}">{% trans "Payers" %}</a>
|
||||
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-cancellation-reason-list' %}">{% trans "Cancellation reasons" %}</a>
|
||||
|
||||
{% include 'lingo/includes/application_list_fragment.html' with title_no_application=_('Regies outside applications') %}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ from django.urls import path
|
|||
|
||||
from .views import appearance as appearance_views
|
||||
from .views import campaign as campaign_views
|
||||
from .views import cancellation_reason as cancellation_reason_views
|
||||
from .views import home as home_views
|
||||
from .views import payer as payer_views
|
||||
from .views import pool as pool_views
|
||||
|
@ -202,6 +203,11 @@ urlpatterns = [
|
|||
regie_views.regie_payment_pdf,
|
||||
name='lingo-manager-invoicing-regie-payment-pdf',
|
||||
),
|
||||
path(
|
||||
'regie/<int:regie_pk>/payment/<int:payment_pk>/cancel/',
|
||||
regie_views.regie_payment_cancel,
|
||||
name='lingo-manager-invoicing-regie-payment-cancel',
|
||||
),
|
||||
path(
|
||||
'regie/<int:regie_pk>/credits/',
|
||||
regie_views.regie_credit_list,
|
||||
|
@ -253,4 +259,24 @@ urlpatterns = [
|
|||
payer_views.payer_export,
|
||||
name='lingo-manager-invoicing-payer-export',
|
||||
),
|
||||
path(
|
||||
'cancellation-reasons/',
|
||||
cancellation_reason_views.reason_list,
|
||||
name='lingo-manager-invoicing-cancellation-reason-list',
|
||||
),
|
||||
path(
|
||||
'cancellation-reason/payment/add/',
|
||||
cancellation_reason_views.payment_reason_add,
|
||||
name='lingo-manager-invoicing-payment-cancellation-reason-add',
|
||||
),
|
||||
path(
|
||||
'cancellation-reason/payment/<int:pk>/edit/',
|
||||
cancellation_reason_views.payment_reason_edit,
|
||||
name='lingo-manager-invoicing-payment-cancellation-reason-edit',
|
||||
),
|
||||
path(
|
||||
'cancellation-reason/payment/<int:pk>/delete/',
|
||||
cancellation_reason_views.payment_reason_delete,
|
||||
name='lingo-manager-invoicing-payment-cancellation-reason-delete',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# lingo - payment and billing system
|
||||
# Copyright (C) 2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.urls import reverse
|
||||
from django.views.generic import CreateView, DeleteView, TemplateView, UpdateView
|
||||
|
||||
from lingo.invoicing.forms import PaymentCancellationReasonForm
|
||||
from lingo.invoicing.models import PaymentCancellationReason
|
||||
|
||||
|
||||
class ReasonListView(TemplateView):
|
||||
template_name = 'lingo/invoicing/manager_cancellation_reason_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.update(
|
||||
{
|
||||
'payment_reason_list': PaymentCancellationReason.objects.all().order_by('disabled', 'label'),
|
||||
pmarillonnet marked this conversation as resolved
Outdated
pmarillonnet
commented
Question d’interface utilisateur, peut-être un Question d’interface utilisateur, peut-être un `order_by('-disabled')` ici, histoire, dans le rendu du gabarit, d’avoir les raisons actives apparaissant en premier, puis à la fin celles qui sont désactivées ?
lguerin
commented
ok, je fais ça (fixup) ok, je fais ça (fixup)
|
||||
}
|
||||
)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
reason_list = ReasonListView.as_view()
|
||||
|
||||
|
||||
class PaymentReasonAddView(CreateView):
|
||||
pmarillonnet marked this conversation as resolved
Outdated
pmarillonnet
commented
Pour quelle raison cette vue de création n’a-t-elle pas recours, comme la vue d’édition, à un formulaire, similaire ou identique au Pour quelle raison cette vue de création n’a-t-elle pas recours, comme la vue d’édition, à un formulaire, similaire ou identique au `PaymentCancellationReasonForm`, qui gérerait la prévention de doublons sur le slug ?
lguerin
commented
le slug est automatiquement setté par la méthode save s'il n'est pas fourni le slug est automatiquement setté par la méthode save s'il n'est pas fourni
pmarillonnet
commented
Ah oui ok j’avais loupé ça, merci. Ah oui ok j’avais loupé ça, merci.
|
||||
template_name = 'lingo/invoicing/manager_payment_cancellation_reason_form.html'
|
||||
model = PaymentCancellationReason
|
||||
fields = ['label']
|
||||
|
||||
def get_success_url(self):
|
||||
return '%s#open:payment' % reverse('lingo-manager-invoicing-cancellation-reason-list')
|
||||
|
||||
|
||||
payment_reason_add = PaymentReasonAddView.as_view()
|
||||
|
||||
|
||||
class PaymentReasonEditView(UpdateView):
|
||||
template_name = 'lingo/invoicing/manager_payment_cancellation_reason_form.html'
|
||||
model = PaymentCancellationReason
|
||||
form_class = PaymentCancellationReasonForm
|
||||
|
||||
def get_success_url(self):
|
||||
return '%s#open:payment' % reverse('lingo-manager-invoicing-cancellation-reason-list')
|
||||
|
||||
|
||||
payment_reason_edit = PaymentReasonEditView.as_view()
|
||||
|
||||
|
||||
class PaymentReasonDeleteView(DeleteView):
|
||||
template_name = 'lingo/manager_confirm_delete.html'
|
||||
model = PaymentCancellationReason
|
||||
|
||||
def get_queryset(self):
|
||||
return PaymentCancellationReason.objects.filter(payment__isnull=True)
|
||||
pmarillonnet marked this conversation as resolved
Outdated
pmarillonnet
commented
J’ai peur qu’on perde des agents en route ici, qu’illes croient à un bug à ne pas voir apparaître ici la raison d’annulation qu’illes souhaitent supprimer, alors qu’en fait cette raison n’y apparaît pas car encore liée à un paiement. Est-ce qu’on aurait pas intérêt, dans un autre ticket, à faire apparaître dans cet écran la liste des raisons encore actives et qui ne peuvent pas être supprimées ? J’ai peur qu’on perde des agents en route ici, qu’illes croient à un bug à ne pas voir apparaître ici la raison d’annulation qu’illes souhaitent supprimer, alors qu’en fait cette raison n’y apparaît pas car encore liée à un paiement.
Est-ce qu’on aurait pas intérêt, dans un autre ticket, à faire apparaître dans cet écran la liste des raisons encore actives et qui ne peuvent pas être supprimées ?
lguerin
commented
en fait on n'affiche pas le lien de suppression s'il existe des paiements liés à ce motif, c'est juste une protection en fait on n'affiche pas le lien de suppression s'il existe des paiements liés à ce motif, c'est juste une protection
pmarillonnet
commented
Ok, encore mieux, nickel, merci. Ok, encore mieux, nickel, merci.
|
||||
|
||||
def get_success_url(self):
|
||||
return '%s#open:payment' % reverse('lingo-manager-invoicing-cancellation-reason-list')
|
||||
|
||||
|
||||
payment_reason_delete = PaymentReasonDeleteView.as_view()
|
|
@ -35,6 +35,7 @@ from lingo.invoicing.forms import (
|
|||
RegieCreditFilterSet,
|
||||
RegieForm,
|
||||
RegieInvoiceFilterSet,
|
||||
RegiePaymentCancelForm,
|
||||
RegiePaymentFilterSet,
|
||||
RegiePublishingForm,
|
||||
RegieRefundFilterSet,
|
||||
|
@ -620,33 +621,64 @@ class RegiePaymentListView(ListView):
|
|||
response['Content-Disposition'] = 'attachment; filename="payments.csv"'
|
||||
writer = csv.writer(response)
|
||||
# headers
|
||||
headers = [
|
||||
_('Number'),
|
||||
_('Invoice number'),
|
||||
_('Date'),
|
||||
_('Payer ID'),
|
||||
_('Payer first name'),
|
||||
_('Payer last name'),
|
||||
_('Payment type'),
|
||||
_('Amount assigned'),
|
||||
_('Total amount'),
|
||||
] + [v for k, v in PAYMENT_INFO]
|
||||
headers = (
|
||||
[
|
||||
_('Number'),
|
||||
_('Invoice number'),
|
||||
_('Date'),
|
||||
_('Payer ID'),
|
||||
_('Payer first name'),
|
||||
_('Payer last name'),
|
||||
_('Payment type'),
|
||||
_('Amount assigned'),
|
||||
_('Total amount'),
|
||||
]
|
||||
+ [v for k, v in PAYMENT_INFO]
|
||||
+ [
|
||||
_('Cancelled on'),
|
||||
pmarillonnet marked this conversation as resolved
Outdated
pmarillonnet
commented
Sur les libellés exposés dans l’UI, je dirais plutôt Sur les libellés exposés dans l’UI, je dirais plutôt `Cancelled on` vu que c’est un datetime et pas juste un heure (à vue de nez “Cancelled on March 26th, 3:43p.m.” m’a l’air plus correct que avec “Cancelled at …”).
lguerin
commented
fixup pour ça fixup pour ça
|
||||
_('Cancellation reason'),
|
||||
]
|
||||
)
|
||||
writer.writerow(headers)
|
||||
for payment in self.object_list:
|
||||
for invoice_payment in payment.get_invoice_payments():
|
||||
invoice_payments = payment.get_invoice_payments()
|
||||
for invoice_payment in invoice_payments:
|
||||
writer.writerow(
|
||||
[
|
||||
payment.formatted_number,
|
||||
invoice_payment.invoice.formatted_number,
|
||||
payment.created_at.date().isoformat(),
|
||||
invoice_payment.invoice.payer_external_id,
|
||||
invoice_payment.invoice.payer_first_name,
|
||||
invoice_payment.invoice.payer_last_name,
|
||||
payment.payer_external_id,
|
||||
payment.payer_first_name,
|
||||
payment.payer_last_name,
|
||||
payment.payment_type.label,
|
||||
invoice_payment.amount,
|
||||
payment.amount,
|
||||
]
|
||||
+ [payment.payment_info.get(k) or '' for k, v in PAYMENT_INFO]
|
||||
+ [
|
||||
payment.cancelled_at.isoformat() if payment.cancelled_at else '',
|
||||
payment.cancellation_reason or '',
|
||||
]
|
||||
)
|
||||
if not invoice_payments:
|
||||
writer.writerow(
|
||||
[
|
||||
payment.formatted_number,
|
||||
'',
|
||||
payment.created_at.date().isoformat(),
|
||||
payment.payer_external_id,
|
||||
payment.payer_first_name,
|
||||
payment.payer_last_name,
|
||||
payment.payment_type.label,
|
||||
'',
|
||||
payment.amount,
|
||||
]
|
||||
+ [payment.payment_info.get(k) or '' for k, v in PAYMENT_INFO]
|
||||
+ [
|
||||
payment.cancelled_at.isoformat() if payment.cancelled_at else '',
|
||||
payment.cancellation_reason or '',
|
||||
]
|
||||
)
|
||||
return response
|
||||
|
||||
|
@ -669,6 +701,38 @@ class RegiePaymentPDFView(PDFMixin, DetailView):
|
|||
regie_payment_pdf = RegiePaymentPDFView.as_view()
|
||||
|
||||
|
||||
class RegiePaymentCancelView(UpdateView):
|
||||
template_name = 'lingo/invoicing/manager_payment_cancel_form.html'
|
||||
pk_url_kwarg = 'payment_pk'
|
||||
model = Payment
|
||||
form_class = RegiePaymentCancelForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(regie=self.regie, cancelled_at__isnull=True)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['regie'] = self.regie
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return '%s?number=%s' % (
|
||||
reverse('lingo-manager-invoicing-regie-payment-list', args=[self.regie.pk]),
|
||||
self.object.formatted_number,
|
||||
)
|
||||
|
||||
|
||||
regie_payment_cancel = RegiePaymentCancelView.as_view()
|
||||
|
||||
|
||||
class RegieCreditListView(ListView):
|
||||
template_name = 'lingo/invoicing/manager_credit_list.html'
|
||||
paginate_by = 100
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import pytest
|
||||
|
||||
from lingo.invoicing.models import Payment, PaymentCancellationReason, PaymentType, Regie
|
||||
from tests.utils import login
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_add_payment_reason(app, admin_user):
|
||||
app = login(app)
|
||||
resp = app.get('/manage/invoicing/regies/')
|
||||
resp = resp.click('Cancellation reasons')
|
||||
resp = resp.click('New payment cancellation reason')
|
||||
resp.form['label'] = 'Foo bar'
|
||||
assert 'slug' not in resp.context['form'].fields
|
||||
assert 'disabled' not in resp.context['form'].fields
|
||||
resp = resp.form.submit()
|
||||
payment_reason = PaymentCancellationReason.objects.latest('pk')
|
||||
assert resp.location.endswith('/manage/invoicing/cancellation-reasons/#open:payment')
|
||||
assert payment_reason.label == 'Foo bar'
|
||||
assert payment_reason.slug == 'foo-bar'
|
||||
assert payment_reason.disabled is False
|
||||
|
||||
resp = app.get('/manage/invoicing/cancellation-reason/payment/add/')
|
||||
resp.form['label'] = 'Foo bar'
|
||||
resp = resp.form.submit()
|
||||
payment_reason = PaymentCancellationReason.objects.latest('pk')
|
||||
assert resp.location.endswith('/manage/invoicing/cancellation-reasons/#open:payment')
|
||||
assert payment_reason.label == 'Foo bar'
|
||||
assert payment_reason.slug == 'foo-bar-1'
|
||||
assert payment_reason.disabled is False
|
||||
|
||||
|
||||
def test_edit_payment_reason(app, admin_user):
|
||||
payment_reason = PaymentCancellationReason.objects.create(label='Foo')
|
||||
payment_reason2 = PaymentCancellationReason.objects.create(label='Baz')
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/invoicing/cancellation-reasons/')
|
||||
resp = resp.click(href='/manage/invoicing/cancellation-reason/payment/%s/edit/' % (payment_reason.pk))
|
||||
resp.form['label'] = 'Foo bar'
|
||||
resp.form['slug'] = payment_reason2.slug
|
||||
resp.form['disabled'] = True
|
||||
resp = resp.form.submit()
|
||||
assert resp.context['form'].errors['slug'] == [
|
||||
'Another payment cancellation reason exists with the same identifier.'
|
||||
]
|
||||
|
||||
resp.form['slug'] = 'foo-bar'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location.endswith('/manage/invoicing/cancellation-reasons/#open:payment')
|
||||
payment_reason.refresh_from_db()
|
||||
assert payment_reason.label == 'Foo bar'
|
||||
assert payment_reason.slug == 'foo-bar'
|
||||
assert payment_reason.disabled is True
|
||||
|
||||
|
||||
def test_delete_payment_reason(app, admin_user):
|
||||
regie = Regie.objects.create(label='Foo')
|
||||
payment_reason = PaymentCancellationReason.objects.create(label='Foo')
|
||||
payment = Payment.objects.create(
|
||||
regie=regie,
|
||||
amount=1,
|
||||
payment_type=PaymentType.objects.create(label='Foo', regie=regie),
|
||||
cancellation_reason=payment_reason,
|
||||
)
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/invoicing/cancellation-reasons/')
|
||||
assert '/manage/invoicing/cancellation-reason/payment/%s/delete/' % payment_reason.pk not in resp
|
||||
app.get('/manage/invoicing/cancellation-reason/payment/%s/delete/' % payment_reason.pk, status=404)
|
||||
|
||||
payment.delete()
|
||||
|
||||
resp = app.get('/manage/invoicing/cancellation-reasons/')
|
||||
resp = resp.click(href='/manage/invoicing/cancellation-reason/payment/%s/delete/' % payment_reason.pk)
|
||||
resp = resp.form.submit()
|
||||
assert resp.location.endswith('/manage/invoicing/cancellation-reasons/#open:payment')
|
||||
assert PaymentCancellationReason.objects.exists() is False
|
|
@ -28,6 +28,7 @@ from lingo.invoicing.models import (
|
|||
JournalLine,
|
||||
Payer,
|
||||
Payment,
|
||||
PaymentCancellationReason,
|
||||
PaymentType,
|
||||
Pool,
|
||||
Refund,
|
||||
|
@ -366,7 +367,6 @@ def test_edit_payment_type(app, admin_user):
|
|||
app = login(app)
|
||||
resp = app.get('/manage/invoicing/regie/%s/parameters/' % regie.pk)
|
||||
resp = resp.click(href='/manage/invoicing/regie/%s/payment-type/%s/edit/' % (regie.pk, payment_type.pk))
|
||||
assert 'This check type is set on some existing payments, modify it with caution.' not in resp
|
||||
resp.form['label'] = 'Foo bar'
|
||||
resp.form['slug'] = payment_type2.slug
|
||||
resp.form['disabled'] = True
|
||||
|
@ -1866,6 +1866,22 @@ def test_regie_payments(app, admin_user):
|
|||
amount=2,
|
||||
)
|
||||
|
||||
payment4 = Payment.objects.create(
|
||||
regie=regie,
|
||||
amount=2,
|
||||
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
|
||||
payer_external_id='payer:3',
|
||||
payer_first_name='First3',
|
||||
payer_last_name='Name3',
|
||||
payer_address='43 rue des kangourous\n99999 Kangourou Ville',
|
||||
cancelled_at=now(),
|
||||
cancelled_by=admin_user,
|
||||
cancellation_reason=PaymentCancellationReason.objects.create(label='Uncovered check'),
|
||||
cancellation_description='foo bar\nblah',
|
||||
)
|
||||
payment4.set_number()
|
||||
payment4.save()
|
||||
|
||||
invoice1.refresh_from_db()
|
||||
assert invoice1.remaining_amount == 0
|
||||
assert invoice1.paid_amount == 40
|
||||
|
@ -1895,6 +1911,7 @@ def test_regie_payments(app, admin_user):
|
|||
invoice1.created_at.strftime('%y-%m'),
|
||||
),
|
||||
'Number: 123456',
|
||||
'Cancel payment',
|
||||
]
|
||||
assert resp.pyquery(
|
||||
'tr[data-invoicing-element-id="%s"]' % payment2.pk
|
||||
|
@ -1921,6 +1938,7 @@ def test_regie_payments(app, admin_user):
|
|||
'Bank/Organism: Bar',
|
||||
'Number: 123456',
|
||||
'Reference: Ref',
|
||||
'Cancel payment',
|
||||
]
|
||||
assert resp.pyquery(
|
||||
'tr[data-invoicing-element-id="%s"]' % payment3.pk
|
||||
|
@ -1938,20 +1956,43 @@ def test_regie_payments(app, admin_user):
|
|||
regie.pk,
|
||||
invoice3.created_at.strftime('%y-%m'),
|
||||
),
|
||||
'Cancel payment',
|
||||
]
|
||||
assert resp.pyquery(
|
||||
'tr[data-invoicing-element-id="%s"]' % payment4.pk
|
||||
).text() == 'Cancelled Payment R%02d-%s-0000004 dated %s from First3 Name3, amount 2.00€ (Check) - download' % (
|
||||
regie.pk,
|
||||
payment4.created_at.strftime('%y-%m'),
|
||||
payment4.created_at.strftime('%d/%m/%Y'),
|
||||
)
|
||||
assert [
|
||||
PyQuery(tr).text() for tr in resp.pyquery('tr[data-related-invoicing-element-id="%s"]' % payment4.pk)
|
||||
] == [
|
||||
'Invoice\nAmount charged\nAmount assigned',
|
||||
'No assignments for this payment',
|
||||
'Cancelled on: %s' % payment4.cancelled_at.strftime('%d/%m/%Y %H:%M'),
|
||||
'Cancelled by: admin',
|
||||
'Reason: Uncovered check',
|
||||
'Description: foo bar\nblah',
|
||||
]
|
||||
|
||||
resp = app.get('/manage/invoicing/regie/%s/payments/?csv' % regie.pk)
|
||||
assert resp.headers['Content-Type'] == 'text/csv'
|
||||
assert resp.headers['Content-Disposition'] == 'attachment; filename="payments.csv"'
|
||||
assert len([a for a in resp.text.split('\r\n') if a]) == 1 + 4
|
||||
assert len([a for a in resp.text.split('\r\n') if a]) == 1 + 5
|
||||
assert resp.text == (
|
||||
'Number,Invoice number,Date,Payer ID,Payer first name,Payer last name,Payment type,Amount assigned,'
|
||||
'Total amount,Issuer,Bank/Organism,Number,Reference\r\n'
|
||||
'R%02d-%s-0000003,F%02d-%s-0000003,%s,payer:3,First3,Name3,Credit card,2.00,2.00,,,,\r\n'
|
||||
'R%02d-%s-0000002,F%02d-%s-0000001,%s,payer:1,First1,Name1,Check,5.00,55.00,Foo,Bar,123456,Ref\r\n'
|
||||
'R%02d-%s-0000002,F%02d-%s-0000002,%s,payer:1,First1,Name1,Check,50.00,55.00,Foo,Bar,123456,Ref\r\n'
|
||||
'R%02d-%s-0000001,F%02d-%s-0000001,%s,payer:1,First1,Name1,Cash,35.00,35.00,,,123456,\r\n'
|
||||
'Total amount,Issuer,Bank/Organism,Number,Reference,Cancelled on,Cancellation reason\r\n'
|
||||
'R%02d-%s-0000004,,%s,payer:3,First3,Name3,Check,,2.00,,,,,%s,Uncovered check\r\n'
|
||||
'R%02d-%s-0000003,F%02d-%s-0000003,%s,payer:3,First3,Name3,Credit card,2.00,2.00,,,,,,\r\n'
|
||||
'R%02d-%s-0000002,F%02d-%s-0000001,%s,payer:1,First1,Name1,Check,5.00,55.00,Foo,Bar,123456,Ref,,\r\n'
|
||||
'R%02d-%s-0000002,F%02d-%s-0000002,%s,payer:1,First1,Name1,Check,50.00,55.00,Foo,Bar,123456,Ref,,\r\n'
|
||||
'R%02d-%s-0000001,F%02d-%s-0000001,%s,payer:1,First1,Name1,Cash,35.00,35.00,,,123456,,,\r\n'
|
||||
) % (
|
||||
regie.pk,
|
||||
payment4.created_at.strftime('%y-%m'),
|
||||
payment4.created_at.strftime('%Y-%m-%d'),
|
||||
payment4.cancelled_at.isoformat(),
|
||||
regie.pk,
|
||||
payment3.created_at.strftime('%y-%m'),
|
||||
regie.pk,
|
||||
|
@ -1978,21 +2019,21 @@ def test_regie_payments(app, admin_user):
|
|||
today = datetime.date.today()
|
||||
params = [
|
||||
({'number': payment1.formatted_number}, 1, 1),
|
||||
({'number': payment1.created_at.strftime('%y-%m')}, 3, 4),
|
||||
({'created_at_after': today.strftime('%Y-%m-%d')}, 3, 4),
|
||||
({'number': payment1.created_at.strftime('%y-%m')}, 4, 5),
|
||||
({'created_at_after': today.strftime('%Y-%m-%d')}, 4, 5),
|
||||
({'created_at_after': (today + datetime.timedelta(days=1)).strftime('%Y-%m-%d')}, 0, 0),
|
||||
({'created_at_before': (today - datetime.timedelta(days=1)).strftime('%Y-%m-%d')}, 0, 0),
|
||||
({'created_at_before': today.strftime('%Y-%m-%d')}, 3, 4),
|
||||
({'created_at_before': today.strftime('%Y-%m-%d')}, 4, 5),
|
||||
({'invoice_number': invoice1.formatted_number}, 2, 3),
|
||||
({'invoice_number': invoice1.created_at.strftime('%y-%m')}, 3, 4),
|
||||
({'payer_external_id': 'payer:1'}, 2, 3),
|
||||
({'payer_external_id': 'payer:3'}, 1, 1),
|
||||
({'payer_first_name': 'first'}, 3, 4),
|
||||
({'payer_external_id': 'payer:3'}, 2, 2),
|
||||
({'payer_first_name': 'first'}, 4, 5),
|
||||
({'payer_first_name': 'first1'}, 2, 3),
|
||||
({'payer_last_name': 'name'}, 3, 4),
|
||||
({'payer_last_name': 'name'}, 4, 5),
|
||||
({'payer_last_name': 'name1'}, 2, 3),
|
||||
({'payment_type': PaymentType.objects.get(slug='cash').pk}, 1, 1),
|
||||
({'payment_type': PaymentType.objects.get(slug='check').pk}, 1, 2),
|
||||
({'payment_type': PaymentType.objects.get(slug='check').pk}, 2, 3),
|
||||
(
|
||||
{
|
||||
'amount_min': '2',
|
||||
|
@ -2006,29 +2047,31 @@ def test_regie_payments(app, admin_user):
|
|||
'amount_min': '2',
|
||||
'amount_min_lookup': 'gte',
|
||||
},
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
),
|
||||
(
|
||||
{
|
||||
'amount_max': '55',
|
||||
'amount_max_lookup': 'lt',
|
||||
},
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
),
|
||||
(
|
||||
{
|
||||
'amount_max': '55',
|
||||
'amount_max_lookup': 'lte',
|
||||
},
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
),
|
||||
({'agenda': 'agenda-a'}, 2, 3),
|
||||
({'agenda': 'agenda-b'}, 1, 2),
|
||||
({'event': 'agenda-a@event-a'}, 2, 3),
|
||||
({'event': 'agenda-b@event-b'}, 1, 2),
|
||||
({'cancelled': 'yes'}, 1, 1),
|
||||
({'cancelled': 'no'}, 3, 4),
|
||||
]
|
||||
for param, result, csv_result in params:
|
||||
resp = app.get(
|
||||
|
@ -2163,6 +2206,129 @@ def test_regie_payment_pdf(app, admin_user):
|
|||
)
|
||||
|
||||
|
||||
def test_regie_payment_cancel(app, admin_user):
|
||||
regie = Regie.objects.create(
|
||||
label='Foo',
|
||||
)
|
||||
PaymentType.create_defaults(regie)
|
||||
invoice1 = Invoice.objects.create(
|
||||
date_publication=datetime.date(2022, 10, 1),
|
||||
date_payment_deadline=datetime.date(2022, 10, 31),
|
||||
date_due=datetime.date(2022, 10, 31),
|
||||
regie=regie,
|
||||
payer_external_id='payer:1',
|
||||
payer_first_name='First1',
|
||||
payer_last_name='Name1',
|
||||
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
|
||||
payer_demat=True,
|
||||
payer_direct_debit=False,
|
||||
)
|
||||
invoice1.set_number()
|
||||
invoice1.save()
|
||||
invoice2 = Invoice.objects.create(
|
||||
date_publication=datetime.date(2022, 10, 1),
|
||||
date_payment_deadline=datetime.date(2022, 10, 31),
|
||||
date_due=datetime.date(2022, 10, 31),
|
||||
date_debit=datetime.date(2022, 11, 15),
|
||||
regie=regie,
|
||||
payer_external_id='payer:1',
|
||||
payer_first_name='First1',
|
||||
payer_last_name='Name1',
|
||||
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
|
||||
payer_demat=False,
|
||||
payer_direct_debit=True,
|
||||
)
|
||||
invoice2.set_number()
|
||||
invoice2.save()
|
||||
|
||||
invoice_line1 = InvoiceLine.objects.create(
|
||||
event_date=datetime.date(2022, 9, 1),
|
||||
invoice=invoice1,
|
||||
quantity=1,
|
||||
unit_amount=40,
|
||||
user_external_id='user:1',
|
||||
user_first_name='User1',
|
||||
user_last_name='Name1',
|
||||
)
|
||||
invoice_line2 = InvoiceLine.objects.create(
|
||||
event_date=datetime.date(2022, 9, 1),
|
||||
invoice=invoice2,
|
||||
quantity=1,
|
||||
unit_amount=50,
|
||||
user_external_id='user:1',
|
||||
user_first_name='User1',
|
||||
user_last_name='Name1',
|
||||
)
|
||||
|
||||
payment = Payment.objects.create(
|
||||
regie=regie,
|
||||
amount=55,
|
||||
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
|
||||
payer_external_id='payer:1',
|
||||
payer_first_name='First1',
|
||||
payer_last_name='Name1',
|
||||
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
|
||||
)
|
||||
payment.set_number()
|
||||
payment.save()
|
||||
InvoiceLinePayment.objects.create(
|
||||
payment=payment,
|
||||
line=invoice_line1,
|
||||
amount=5,
|
||||
)
|
||||
InvoiceLinePayment.objects.create(
|
||||
payment=payment,
|
||||
line=invoice_line2,
|
||||
amount=50,
|
||||
)
|
||||
cancellation_reason = PaymentCancellationReason.objects.create(label='Uncovered check')
|
||||
PaymentCancellationReason.objects.create(label='Disabled', disabled=True)
|
||||
payment2 = Payment.objects.create(
|
||||
regie=regie,
|
||||
amount=5,
|
||||
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
|
||||
payer_external_id='payer:1',
|
||||
payer_first_name='First1',
|
||||
payer_last_name='Name1',
|
||||
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
|
||||
)
|
||||
payment2.set_number()
|
||||
payment2.save()
|
||||
InvoiceLinePayment.objects.create(
|
||||
payment=payment2,
|
||||
line=invoice_line1,
|
||||
amount=5,
|
||||
)
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/invoicing/regie/%s/payments/' % regie.pk)
|
||||
resp = resp.click(href='/manage/invoicing/regie/%s/payment/%s/cancel/' % (regie.pk, payment.pk))
|
||||
assert resp.form['cancellation_reason'].options == [
|
||||
('', True, '---------'),
|
||||
(str(cancellation_reason.pk), False, 'Uncovered check'),
|
||||
]
|
||||
resp.form['cancellation_reason'] = cancellation_reason.pk
|
||||
resp.form['cancellation_description'] = 'foo bar blah'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location.endswith(
|
||||
'/manage/invoicing/regie/%s/payments/?number=%s' % (regie.pk, payment.formatted_number)
|
||||
)
|
||||
payment.refresh_from_db()
|
||||
assert payment.cancelled_at is not None
|
||||
assert payment.cancelled_by == admin_user
|
||||
assert payment.cancellation_reason == cancellation_reason
|
||||
assert payment.cancellation_description == 'foo bar blah'
|
||||
assert payment.invoicelinepayment_set.count() == 0
|
||||
assert payment2.invoicelinepayment_set.count() == 1
|
||||
|
||||
# already cancelled
|
||||
app.get('/manage/invoicing/regie/%s/payment/%s/cancel/' % (regie.pk, payment.pk), status=404)
|
||||
payment.cancelled_at = None
|
||||
payment.save()
|
||||
other_regie = Regie.objects.create(label='Foo')
|
||||
app.get('/manage/invoicing/regie/%s/payment/%s/cancel/' % (other_regie.pk, payment.pk), status=404)
|
||||
|
||||
|
||||
def test_regie_credits(app, admin_user):
|
||||
regie = Regie.objects.create(label='Foo')
|
||||
Agenda.objects.create(label='Agenda A', regie=regie)
|
||||
|
|
J’imagine qu’il y a une bonne raison pour ne pas avoir posé cette contrainte d’unicité dans le champ directement (genre avec un
unique=True
à la définition du champ) mais je ne saisis pas laquelle, je viens bien la savoir ne serait-ce que pour ma propre édification personnelle stp :)non c'est une erreur, merci :) (fixup)