facturation: gestion des bordereaux de remise de règlements (#88698) #177

Merged
lguerin merged 11 commits from wip/88698-invoicing-docket into main 2024-04-08 09:57:36 +02:00
20 changed files with 3791 additions and 2396 deletions

View File

@ -39,6 +39,7 @@ from lingo.invoicing.models import (
Payer,
Payment,
PaymentCancellationReason,
PaymentDocket,
PaymentType,
Refund,
Regie,
@ -845,6 +846,100 @@ class RegiePaymentCancelForm(forms.ModelForm):
return self.instance
class RegieDocketPaymentFilterSet(django_filters.FilterSet):
payment_type = django_filters.MultipleChoiceFilter(
label=_('Payment type'),
widget=forms.CheckboxSelectMultiple,
required=True,
)
date_end = django_filters.DateFilter(
label=_('Date end'),
widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
required=True,
field_name='created_at',
lookup_expr='lt',
)
class Meta:
model = Payment
fields = []
def __init__(self, *args, **kwargs):
self.regie = kwargs.pop('regie')
if kwargs.get('data') is None:
# set initial through data, so form is valid on page load
pmarillonnet marked this conversation as resolved Outdated

Je ne connaissais pas l’astuce, ok :)

Je ne connaissais pas l’astuce, ok :)

Oui, je ne suis pas super fière de moi, c'est un peu moche; mais en faisant avec initial ça ne valide pas le formulaire et donc pas de résultat du filtrage.
L'idée étant d'arriver sur la page avec déjà tous les paiements non remis listés

Oui, je ne suis pas super fière de moi, c'est un peu moche; mais en faisant avec initial ça ne valide pas le formulaire et donc pas de résultat du filtrage. L'idée étant d'arriver sur la page avec déjà tous les paiements non remis listés

Oui je vois, rien à redire et de toute façon je n’ai pas mieux à proposer, ok pour moi. Je marque comme résolue cette conversation.

Oui je vois, rien à redire et de toute façon je n’ai pas mieux à proposer, ok pour moi. Je marque comme résolue cette conversation.
kwargs['data'] = {
'payment_type': [p.pk for p in self.regie.paymenttype_set.all()],
'date_end': now().date(),
}
super().__init__(*args, **kwargs)
self.filters['payment_type'].field.choices = [(t.pk, t) for t in self.regie.paymenttype_set.all()]
class PaymentDocketForm(forms.ModelForm):
class Meta:
model = PaymentDocket
fields = ['payment_types', 'date_end']
widgets = {
'payment_types': forms.CheckboxSelectMultiple,
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
}
def __init__(self, *args, **kwargs):
self.regie = kwargs.pop('regie')
if kwargs.get('data') is None:
# set initial through data, so form is valid on page load
kwargs['data'] = {
'payment_types': [p.pk for p in self.regie.paymenttype_set.all()],
'date_end': now().date(),
}
super().__init__(*args, **kwargs)
self.fields['payment_types'].queryset = self.regie.paymenttype_set.all()
def save(self):
self.instance = super().save()
filterset = RegieDocketPaymentFilterSet(
regie=self.regie,
queryset=Payment.objects.filter(regie=self.regie, docket__isnull=True, cancelled_at__isnull=True),
data={
'payment_type': [p.pk for p in self.instance.payment_types.all()],
'date_end': self.instance.date_end,
},
)
if filterset.form.is_valid():
payment_queryset = filterset.qs
payment_queryset.update(docket=self.instance)
return self.instance
class PaymentDocketPaymentTypeForm(forms.ModelForm):
additionnal_information = forms.CharField(
label=_('Additionnal information'),
widget=forms.Textarea,
required=False,
)
class Meta:
model = PaymentDocket
fields = []
def __init__(self, *args, **kwargs):
self.payment_type = kwargs.pop('payment_type')
super().__init__(*args, **kwargs)
self.initial['additionnal_information'] = (
self.instance.payment_types_info.get(self.payment_type.slug) or ''
)
def save(self):
super().save(commit=False)
self.instance.payment_types_info[self.payment_type.slug] = self.cleaned_data[
'additionnal_information'
]
self.instance.save()
return self.instance
class RegieCreditFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet):
number = django_filters.CharFilter(
label=_('Credit number'),

View File

@ -0,0 +1,65 @@
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0084_payment_cancellation'),
]
operations = [
migrations.AddField(
model_name='regie',
name='docket_number_format',
field=models.CharField(
default='B{regie_id:02d}-{yy}-{mm}-{number:07d}',
max_length=100,
pmarillonnet marked this conversation as resolved Outdated

Pas compris l’intérêt de limiter autant ce format sur le modèle de régie, lequel est a priori instancié en un nombre d’objets très limité. N’est-ce pas uniquement la longueur de l’identifiant généré à partir de ce format qu’il convient de contrôler ?

Pas compris l’intérêt de limiter autant ce format sur le modèle de régie, lequel est a priori instancié en un nombre d’objets très limité. N’est-ce pas uniquement la longueur de l’identifiant généré à partir de ce format qu’il convient de contrôler ?

pas réfléchi plus que ça, les autres compteurs aussi ont un max_length à 100, et les formatted_number ont un max_length à 200, ce qui devrait suffire

pas réfléchi plus que ça, les autres compteurs aussi ont un max_length à 100, et les formatted_number ont un max_length à 200, ce qui devrait suffire

Ok.

Ok.
verbose_name='Payment docket number format',
),
),
migrations.AlterField(
model_name='counter',
name='kind',
field=models.CharField(
choices=[
('invoice', 'Invoice'),
('payment', 'Payment'),
('credit', 'Credit'),
('refund', 'Refund'),
('docket', 'Payment Docket'),
],
max_length=10,
),
),
migrations.CreateModel(
name='PaymentDocket',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('number', models.PositiveIntegerField(default=0)),
('formatted_number', models.CharField(max_length=200)),
('date_end', models.DateField(verbose_name='End date')),
('draft', models.BooleanField()),
pmarillonnet marked this conversation as resolved Outdated

Peut-être is_draft (pour ne pas croire à tort que ce champ est un accesseur vers les brouillons antérieurs du bordereau) ?

Peut-être `is_draft` (pour ne pas croire à tort que ce champ est un accesseur vers les brouillons antérieurs du bordereau) ?

on a draft aussi sur les campagnes, et tu ne l'as pas relevé :)
je peux changer ici si tu insistes

on a draft aussi sur les campagnes, et tu ne l'as pas relevé :) je peux changer ici si tu insistes

Non, je n’ai pas vu que c’était ainsi par ailleurs, et je relève mais n’insiste pas :)
Laissons ainsi.

Non, je n’ai pas vu que c’était ainsi par ailleurs, et je relève mais n’insiste pas :) Laissons ainsi.
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('payment_types', models.ManyToManyField(to='invoicing.PaymentType')),
('payment_types_info', models.JSONField(blank=True, default=dict)),
(
'regie',
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='invoicing.regie'),
),
],
),
migrations.AddField(
model_name='payment',
pmarillonnet marked this conversation as resolved Outdated

Je vais voir si je capte mieux dans la suite de la relecture, mais je ne comprends pas le choix de la FK dans ce sens et pas l’inverse. L’intuition me dit que c’est le paiement l’objet principal, vers lequel le bordereau, en tant qu’objet secondaire, pointe.

Edit: Ok, je comprends mieux à la lecture de la suite de la PR.

Je vais voir si je capte mieux dans la suite de la relecture, mais je ne comprends pas le choix de la FK dans ce sens et pas l’inverse. L’intuition me dit que c’est le paiement l’objet principal, vers lequel le bordereau, en tant qu’objet secondaire, pointe. Edit: Ok, je comprends mieux à la lecture de la suite de la PR.

un payment ne peut être inclus que dans un seul bordereau :)

un payment ne peut être inclus que dans un seul bordereau :)

Oui j’ai capté ça en cours de route, c’est bon pour moi.

Oui j’ai capté ça en cours de route, c’est bon pour moi.
name='docket',
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.PROTECT, to='invoicing.paymentdocket'
),
),
]

View File

@ -52,10 +52,6 @@ from lingo.utils.wcs import (
)
class RegieImportError(Exception):
pass
class PoolPromotionError(Exception):
def __init__(self, msg):
self.msg = msg
@ -329,6 +325,11 @@ class Regie(WithApplicationMixin, models.Model):
default='R{regie_id:02d}-{yy}-{mm}-{number:07d}',
max_length=100,
)
docket_number_format = models.CharField(
_('Payment docket number format'),
default='B{regie_id:02d}-{yy}-{mm}-{number:07d}',
max_length=100,
)
credit_number_format = models.CharField(
_('Credit number format'),
default='A{regie_id:02d}-{yy}-{mm}-{number:07d}',
@ -385,6 +386,17 @@ class Regie(WithApplicationMixin, models.Model):
'cashier': self.cashier_role.name if self.cashier_role else None,
},
'payer': self.payer.slug if self.payer else None,
'counter_name': self.counter_name,
'invoice_number_format': self.invoice_number_format,
'payment_number_format': self.payment_number_format,
'docket_number_format': self.docket_number_format,
'credit_number_format': self.credit_number_format,
'refund_number_format': self.refund_number_format,
'invoice_model': self.invoice_model,
'invoice_custom_text': self.invoice_custom_text,
'invoice_main_colour': self.invoice_main_colour,
'cashier_name': self.cashier_name,
'city_name': self.city_name,
'payment_types': [p.export_json() for p in self.paymenttype_set.all()],
}
@ -399,10 +411,8 @@ class Regie(WithApplicationMixin, models.Model):
if role_name:
try:
data['cashier_role'] = Group.objects.get(name=role_name)
except Group.DoesNotExists:
raise RegieImportError('Missing role: %s' % role_name)
except Group.MultipleObjectsReturned:
pmarillonnet marked this conversation as resolved Outdated

Ok, je ne savais pas que la définition du Group de django.contrib.auth impose l’unicité sur le nom, top.

Ok, je ne savais pas que la définition du `Group` de `django.contrib.auth` impose l’unicité sur le nom, top.
raise RegieImportError('Multiple role exist with the name: %s' % role_name)
except Group.DoesNotExist:
raise LingoImportError('Missing role: %s' % role_name)
if data['payer']:
try:
data['payer'] = Payer.objects.get(slug=data['payer'])
@ -852,6 +862,7 @@ class Counter(models.Model):
('payment', _('Payment')),
('credit', _('Credit')),
('refund', _('Refund')),
('docket', _('Payment Docket')),
],
)
@ -1335,6 +1346,53 @@ class PaymentCancellationReason(models.Model):
return slugify(self.label)
class PaymentDocket(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
regie = models.ForeignKey(Regie, on_delete=models.PROTECT)
number = models.PositiveIntegerField(default=0)
formatted_number = models.CharField(max_length=200)
date_end = models.DateField(_('End date'))
draft = models.BooleanField()
payment_types = models.ManyToManyField(PaymentType)
payment_types_info = models.JSONField(blank=True, default=dict)
pmarillonnet marked this conversation as resolved Outdated

Note pour moi-même, aller voir à quoi ça correspond dans le code ajouté par les commits successifs.

Note pour moi-même, aller voir à quoi ça correspond dans le code ajouté par les commits successifs.

Ok, davantage capté, à la lecture des commits successifs, à quoi sert ce JSONField.

Ok, davantage capté, à la lecture des commits successifs, à quoi sert ce JSONField.
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
if self.draft:
return '%s-%s' % (_('TEMPORARY'), self.pk)
pmarillonnet marked this conversation as resolved Outdated

Est-ce qu’on a pas intérêt à gérer (dans un autre ticket) aussi un format de brouillon paramétrable ? Est-ce qu’on est certain que le format fixé à 'TEMPORARY-' va convenir à coup sûr ?

Est-ce qu’on a pas intérêt à gérer (dans un autre ticket) aussi un format de brouillon paramétrable ? Est-ce qu’on est certain que le format fixé à 'TEMPORARY-<pk>' va convenir à coup sûr ?

on numérote les factures temporaires aussi comme ça, et on ne veut pas consommer un compteur pour un draft

on numérote les factures temporaires aussi comme ça, et on ne veut pas consommer un compteur pour un draft

Ah oui il y a la gestion des compteurs aussi, c’est vrai. Ok.

Ah oui il y a la gestion des compteurs aussi, c’est vrai. Ok.
return self.formatted_number
def set_number(self):
self.number = Counter.get_count(
regie=self.regie,
name=self.regie.get_counter_name(self.created_at),
kind='docket',
)
self.formatted_number = self.regie.format_number(self.created_at, self.number, 'docket')
def get_active_payments(self):
result = []
for payment_type in self.payment_types.all():
qs = self.payment_set.filter(payment_type=payment_type, cancelled_at__isnull=True).select_related(
'payment_type'
)
result.append(
{
'payment_type': payment_type,
'list': qs.order_by('-created_at'),
'amount': qs.aggregate(amount=models.Sum('amount')),
}
)
return result
def get_cancelled_payments(self):
qs = self.payment_set.filter(cancelled_at__isnull=False).select_related('payment_type')
return {'list': qs.order_by('-created_at'), 'amount': qs.aggregate(amount=models.Sum('amount'))}
PAYMENT_INFO = [
('check_issuer', _('Issuer')),
('check_bank', _('Bank/Organism')),
@ -1366,6 +1424,8 @@ class Payment(models.Model):
)
cancellation_description = models.TextField(_('Description'), blank=True)
docket = models.ForeignKey(PaymentDocket, on_delete=models.PROTECT, null=True)
order_date = models.DateTimeField(null=True)
created_at = models.DateTimeField(auto_now_add=True)

View File

@ -0,0 +1,83 @@
{% extends "lingo/invoicing/manager_docket_list.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-regie-docket-detail' regie.pk object.pk %}">{{ object }}</a>
{% endblock %}
{% block appbar %}
<h2>{{ object }}</h2>
{% if object.draft %}
<span class="actions">
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
<li><a href="{% url 'lingo-manager-invoicing-regie-docket-delete' regie_pk=regie.pk pk=object.pk %}" rel="popup">{% trans "Delete" %}</a></li>
</ul>
</span>
{% endif %}
{% endblock %}
{% block content %}
{% url 'lingo-manager-invoicing-regie-payment-list' regie_pk=regie.pk as regie_payment_list_url %}
{% for value in object.get_active_payments %}
{% if value.list %}
<div class="section">
<h3>
{% if object.draft %}
<a href="{% url 'lingo-manager-invoicing-regie-docket-payment-type-edit' regie_pk=regie.pk pk=object.pk payment_type_pk=value.payment_type.pk %}">{{ value.payment_type }}</a>
{% else %}
{{ value.payment_type }}
{% endif %}
</h3>
<div>
<p>
{% trans "Number of payments:" %} {{ value.list|length }}
<br />
{% trans "Total amount:" %} {% blocktrans with amount=value.amount.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}
<br />
{% trans "Additionnal information:" %}
<br />
{{ object.payment_types_info|get:value.payment_type.slug|default:""|linebreaksbr }}
</p>
<table class="main pk-compact-table invoicing-element-list">
{% for payment in value.list %}
<tr>
<td colspan="4">
{% 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 <a href="{{ regie_payment_list_url }}?number={{ payment_number }}">{{ payment_number }}</a> dated {{ cdate }} from {{ payer_name }}, amount {{ amount }}€ ({{ payment_type }}){% endblocktrans %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endif %}
{% endfor %}
{% with object.get_cancelled_payments as cancelled %}
{% if cancelled.list %}
<div class="section">
<h3>{% trans "Cancelled payments" %}</h3>
<div>
<p>
{% trans "Number of payments:" %} {{ cancelled.list|length }}
<br />
{% trans "Total amount:" %} {% blocktrans with amount=cancelled.amount.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}
</p>
<table class="main pk-compact-table invoicing-element-list">
{% for payment in cancelled.list %}
<tr>
<td colspan="4">
{% 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 <a href="{{ regie_payment_list_url }}?number={{ payment_number }}">{{ payment_number }}</a> dated {{ cdate }} from {{ payer_name }}, amount {{ amount }}€ ({{ payment_type }}){% endblocktrans %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endif %}
{% endwith %}
{% endblock %}
{% block sidebar %}
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "lingo/invoicing/manager_docket_payment_list.html" %}
{% load gadjo i18n %}
{% block breadcrumb %}
{{ block.super }}
{% if form.instance.pk %}
<a href="{# url 'lingo-manager-invoicing-regie-docket-edit' regie_pk=regie.pk pk=object.pk #}">{% trans "Edit" %}</a>
{% else %}
<a href="{% url 'lingo-manager-invoicing-regie-docket-add' regie_pk=regie.pk %}">{% trans "New docket" %}</a>
{% endif %}
{% endblock %}
{% block appbar %}
{% if object.pk %}
<h2>{% trans "Edit docket" %}</h2>
{% else %}
<h2>{% trans "New docket" %}</h2>
{% endif %}
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|with_template }}
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
<a class="cancel" href="{% url 'lingo-manager-invoicing-regie-docket-payment-list' regie.pk %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}
{% block sidebar %}
{% endblock %}

View File

@ -0,0 +1,55 @@
{% extends "lingo/invoicing/manager_docket_payment_list.html" %}
{% load gadjo i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-regie-docket-list' regie_pk=regie.pk %}">{% trans "Dockets" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Dockets" %}</h2>
{% endblock %}
{% block content %}
{% if object_list %}
<div>
<table class="main pk-compact-table invoicing-element-list">
<thead>
<tr>
<th>{% trans "Number" %}</th>
<th>{% trans "Payment types" %}</th>
<th>{% trans "Number of payments" %}</th>
<th>{% trans "End date" %}</th>
</tr>
</thead>
<tbody>
{% for docket in object_list %}
<tr>
<td>
<a href="{% url 'lingo-manager-invoicing-regie-docket-detail' regie_pk=regie.pk pk=docket.pk %}">{{ docket }}</a>
</td>
<td>
{% for payment_type in docket.payment_types.all %}{{ payment_type }}{% if not forloop.last %}, {% endif %}{% endfor %}
</td>
<td>
{% if docket.active_count %}<span class="meta meta-success">{{ docket.active_count }} ({% blocktrans with amount=docket.active_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %})</span>{% endif %}
pmarillonnet marked this conversation as resolved Outdated

Ici juste ça va afficher un chiffre suivi d’un montant en euros entre parenthèses. Niveau UI c’est les classes meta-success et meta-warning qui permettent de distinguer si on parle de paiements actifs ou de paiements annulés ?
Est-ce qu’on aurait pas intérêt à préciser à chaque fois, en toutes lettres, dans lequel des deux cas on se situe, genre 2 actifs (34.34 €) ou 7 annulés (187.87 €) ?

Ici juste ça va afficher un chiffre suivi d’un montant en euros entre parenthèses. Niveau UI c’est les classes `meta-success` et `meta-warning` qui permettent de distinguer si on parle de paiements actifs ou de paiements annulés ? Est-ce qu’on aurait pas intérêt à préciser à chaque fois, en toutes lettres, dans lequel des deux cas on se situe, genre `2 actifs (34.34 €)` ou `7 annulés (187.87 €)` ?

ça a été vu avec stef, il a validé en l'état; sur les campagnes aussi on a juste des nombres et une différence de couleur
on pourra toujours revenir dessus plus tard si besoin :)

ça a été vu avec stef, il a validé en l'état; sur les campagnes aussi on a juste des nombres et une différence de couleur on pourra toujours revenir dessus plus tard si besoin :)

Ok, très bien alors, laissons comme ça.

Ok, très bien alors, laissons comme ça.
{% if docket.cancelled_count %}<span class="meta meta-warning">{{ docket.cancelled_count }} ({% blocktrans with amount=docket.cancelled_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %})</span>{% endif %}
</td>
<td>
{{ docket.date_end|date:"d/m/Y" }}
</td>
</tr>
</tbody>
{% endfor %}
</table>
{% include "gadjo/pagination.html" %}
</div>
{% else %}
<div class="big-msg-info">
{% trans "This site doesn't have any docket yet." %}
</div>
{% endif %}
{% endblock %}
{% block sidebar %}
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends "lingo/invoicing/manager_regie_detail.html" %}
{% load gadjo i18n %}
{% block page-title-extra-label %}{% trans "Dockets" %} | {{ block.super }}{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-regie-docket-payment-list' regie_pk=regie.pk %}">{% trans "Payments outside dockets" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Payments outside dockets" %}</h2>
{% endblock %}
{% block content %}
<div class="section">
<div>
<form class="invoicing-element-filters">
<fieldset id="filters">
<legend>{% trans "Payments outside dockets search" %}</legend>
<div>
{{ filterset.form|with_template }}
<button class="submit-button">{% trans "Search" context 'form filtering action' %}</button>
</div>
</fieldset>
</form>
</div>
</div>
<div>
<table class="main pk-compact-table invoicing-element-list">
{% url 'lingo-manager-invoicing-regie-payment-list' regie_pk=regie.pk as regie_payment_list_url %}
{% for payment in object_list %}
<tr>
<td colspan="4">
{% 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 <a href="{{ regie_payment_list_url }}?number={{ payment_number }}">{{ payment_number }}</a> dated {{ cdate }} from {{ payer_name }}, amount {{ amount }}€ ({{ payment_type }}){% endblocktrans %}
</td>
</tr>
{% endfor %}
</table>
{% include "gadjo/pagination.html" %}
</div>
{% endblock %}
{% block sidebar %}
<aside id="sidebar">
<h3>{% trans "Actions" %}</h3>
pmarillonnet marked this conversation as resolved Outdated

Peut-être ici un {% else %} avec un message indiquant que l’action de création d’un nouveau bordereau n’est pas disponible car il y a déjà des bordereaux à l’état de brouillons ? (Je vois déjà arriver les tickets clients “L’action « Nouveau bordereau » a disparu”).

Peut-être ici un {% else %} avec un message indiquant que l’action de création d’un nouveau bordereau n’est pas disponible car il y a déjà des bordereaux à l’état de brouillons ? (Je vois déjà arriver les tickets clients “L’action « Nouveau bordereau » a disparu”).

je fais ça dans un fixup (avec une copie d'écran attaché à la PR)

je fais ça dans un fixup (avec une copie d'écran attaché à la PR)

Top, je te remercie.

Top, je te remercie.
{% if not has_draft %}
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-regie-docket-add' regie_pk=regie.pk %}?{{ request.GET.urlencode }}">{% trans "New docket" %}</a>
{% else %}
<div class="paragraph">
{% trans "The action to create a new docket is not available because a draft docket already exists." %}
</div>
{% endif %}
<h3>{% trans "Navigation" %}</h3>
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-regie-docket-list' regie_pk=regie.pk %}">{% trans "Dockets" %}</a>
</aside>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "lingo/invoicing/manager_docket_detail.html" %}
{% load gadjo i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-regie-docket-payment-type-edit' regie_pk=regie.pk pk=object.pk payment_type_pk=form.payment_type.pk %}">{{ form.payment_type }}</a>
{% endblock %}
{% block appbar %}
<h2>{{ form.payment_type }}</h2>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|with_template }}
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
<a class="cancel" href="{% url 'lingo-manager-invoicing-regie-docket-detail' regie.pk object.pk %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}
{% block sidebar %}
{% endblock %}

View File

@ -74,6 +74,14 @@
</td>
</tr>
{% endfor %}
{% if payment.docket %}
<tr class="line" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
<td colspan="2"></td>
<td class="payment-details" colspan="3">
<i>{% trans "Docket:" %} <a href="{% url 'lingo-manager-invoicing-regie-docket-detail' regie_pk=regie.pk pk=payment.docket.pk %}">{{ payment.docket }}</a></i>
</td>
</tr>
{% endif %}
{% if not payment.cancelled_at %}
<tr class="line last-line" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
<td colspan="2"></td>

View File

@ -50,6 +50,7 @@
<h3>{% trans "Navigation" %}</h3>
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-regie-invoice-list' regie_pk=regie.pk %}">{% trans 'Invoices' %}</a>
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-regie-payment-list' regie_pk=regie.pk %}">{% trans 'Payments' %}</a>
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-regie-docket-payment-list' regie_pk=regie.pk %}">{% trans 'Dockets' %}</a>
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-regie-credit-list' regie_pk=regie.pk %}">{% trans 'Credits' %}</a>
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-regie-refund-list' regie_pk=regie.pk %}">{% trans 'Refunds' %}</a>
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-non-invoiced-line-list' regie_pk=regie.pk %}">{% trans 'Non invoiced lines' %}</a>

View File

@ -53,6 +53,8 @@
<dd><code>{{ regie.invoice_number_format }}</code></dd>
<dt><b>{% trans "Payment number format:" %}</b></dt>
<dd><code>{{ regie.payment_number_format }}</code></dd>
<dt><b>{% trans "Payment docket number format:" %}</b></dt>
<dd><code>{{ regie.docket_number_format }}</code></dd>
<dt><b>{% trans "Credit number format:" %}</b></dt>
<dd><code>{{ regie.credit_number_format }}</code></dd>
<dt><b>{% trans "Refund number format:" %}</b></dt>

View File

@ -208,6 +208,36 @@ urlpatterns = [
regie_views.regie_payment_cancel,
name='lingo-manager-invoicing-regie-payment-cancel',
),
path(
'regie/<int:regie_pk>/dockets/payments/',
regie_views.regie_docket_payment_list,
name='lingo-manager-invoicing-regie-docket-payment-list',
),
path(
'regie/<int:regie_pk>/dockets/',
regie_views.regie_docket_list,
name='lingo-manager-invoicing-regie-docket-list',
),
path(
pmarillonnet marked this conversation as resolved Outdated

Je loupe peut-être un truc mais j’ai l’impression qu’il manquerait une vue pour éditer un bordereau existant (je vois bien des vues du genre DocketAdd, DocketDelete, DocketList et DocketDetails pas mais de vue DocketEdit). C’est volontaire genre c’est un objet qu’on n’est pas censé éditer directement ainsi (seulement le type de paiement via RegieDocketPaymentTypeEditView), ou bien c’est un oubli ?

Je loupe peut-être un truc mais j’ai l’impression qu’il manquerait une vue pour éditer un bordereau existant (je vois bien des vues du genre DocketAdd, DocketDelete, DocketList et DocketDetails pas mais de vue DocketEdit). C’est volontaire genre c’est un objet qu’on n’est pas censé éditer directement ainsi (seulement le type de paiement via `RegieDocketPaymentTypeEditView`), ou bien c’est un oubli ?

Ah, c’est la PR suivante, #88699, top.

Ah, c’est la PR suivante, #88699, top.
'regie/<int:regie_pk>/docket/add/',
regie_views.regie_docket_add,
name='lingo-manager-invoicing-regie-docket-add',
),
path(
'regie/<int:regie_pk>/docket/<int:pk>/',
regie_views.regie_docket_detail,
name='lingo-manager-invoicing-regie-docket-detail',
),
path(
'regie/<int:regie_pk>/docket/<int:pk>/payment-type/<int:payment_type_pk>/',
regie_views.regie_docket_payment_type_edit,
name='lingo-manager-invoicing-regie-docket-payment-type-edit',
),
path(
'regie/<int:regie_pk>/docket/<int:pk>/delete/',
regie_views.regie_docket_delete,
name='lingo-manager-invoicing-regie-docket-delete',
),
path(
'regie/<int:regie_pk>/credits/',
regie_views.regie_credit_list,

View File

@ -19,8 +19,21 @@ import csv
import datetime
from django.db import transaction
from django.db.models import CharField, IntegerField, JSONField, Prefetch, Q, Value
from django.http import HttpResponse
from django.db.models import (
CharField,
Count,
DecimalField,
IntegerField,
JSONField,
OuterRef,
Prefetch,
Q,
Subquery,
Sum,
Value,
)
from django.db.models.functions import Coalesce
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import yesno
from django.urls import reverse
@ -30,8 +43,11 @@ from django.views.generic import CreateView, DeleteView, DetailView, ListView, U
from lingo.agendas.models import Agenda
from lingo.export_import.views import WithApplicationsMixin
from lingo.invoicing.forms import (
PaymentDocketForm,
PaymentDocketPaymentTypeForm,
PaymentTypeForm,
RegieCreditFilterSet,
RegieDocketPaymentFilterSet,
RegieForm,
RegieInvoiceFilterSet,
RegiePaymentCancelForm,
@ -49,6 +65,7 @@ from lingo.invoicing.models import (
InvoiceLinePayment,
JournalLine,
Payment,
PaymentDocket,
PaymentType,
Pool,
Refund,
@ -159,6 +176,7 @@ class RegieCountersEditView(UpdateView):
'counter_name',
'invoice_number_format',
'payment_number_format',
'docket_number_format',
'credit_number_format',
'refund_number_format',
]
@ -598,6 +616,7 @@ class RegiePaymentListView(ListView):
queryset=invoice_line_payment_queryset,
to_attr='prefetched_invoicelinepayments',
),
'docket',
)
.order_by('-created_at'),
regie=self.regie,
@ -733,6 +752,204 @@ class RegiePaymentCancelView(UpdateView):
regie_payment_cancel = RegiePaymentCancelView.as_view()
class RegieDocketPaymentListView(ListView):
template_name = 'lingo/invoicing/manager_docket_payment_list.html'
paginate_by = 100
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):
self.filterset = RegieDocketPaymentFilterSet(
data=self.request.GET or None,
pmarillonnet marked this conversation as resolved Outdated

J’ai du mal à comprendre le besoin fonctionnel d’exposer une liste des “paiements hors bordereaux” (“payments outside dockets”). Ça correspond à quoi en réalité ?

Edit: ok, pigé dans la suite de la relecture des commits successifs de cette PR.

J’ai du mal à comprendre le besoin fonctionnel d’exposer une liste des “paiements hors bordereaux” (“payments outside dockets”). Ça correspond à quoi en réalité ? Edit: ok, pigé dans la suite de la relecture des commits successifs de cette PR.
queryset=Payment.objects.filter(regie=self.regie, docket__isnull=True, cancelled_at__isnull=True)
.select_related('payment_type')
.order_by('-created_at'),
regie=self.regie,
)
if not self.filterset.form.is_valid():
return Payment.objects.none()
return self.filterset.qs
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['filterset'] = self.filterset
kwargs['has_draft'] = self.regie.paymentdocket_set.filter(draft=True).exists()
return super().get_context_data(**kwargs)
regie_docket_payment_list = RegieDocketPaymentListView.as_view()
class RegieDocketListView(ListView):
template_name = 'lingo/invoicing/manager_docket_list.html'
model = PaymentDocket
paginate_by = 100
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):
payments = Payment.objects.filter(docket=OuterRef('pk')).order_by().values('docket')
active_count = (
payments.filter(cancelled_at__isnull=True).annotate(count=Count('docket')).values('count')
)
active_amount = (
payments.filter(cancelled_at__isnull=True).annotate(total=Sum('amount')).values('total')
)
cancelled_count = (
payments.filter(cancelled_at__isnull=False).annotate(count=Count('docket')).values('count')
)
cancelled_amount = (
payments.filter(cancelled_at__isnull=False).annotate(total=Sum('amount')).values('total')
)
return (
self.regie.paymentdocket_set.all()
.prefetch_related('payment_types')
.annotate(
active_count=Coalesce(Subquery(active_count, output_field=IntegerField()), Value(0)),
cancelled_count=Coalesce(Subquery(cancelled_count, output_field=IntegerField()), Value(0)),
pmarillonnet marked this conversation as resolved Outdated

On écrase les centimes ici en envoyant ça dans un IntegerField au lieu d’un flottant, non ? (pareil pour la ligne du dessous)

On écrase les centimes ici en envoyant ça dans un `IntegerField` au lieu d’un flottant, non ? (pareil pour la ligne du dessous)

bien vu, merci

bien vu, merci

fixup

fixup
active_amount=Coalesce(
Subquery(active_amount, output_field=DecimalField(max_digits=9, decimal_places=2)),
Value(0),
output_field=DecimalField(max_digits=9, decimal_places=2),
),
cancelled_amount=Coalesce(
Subquery(cancelled_amount, output_field=DecimalField(max_digits=9, decimal_places=2)),
Value(0),
output_field=DecimalField(max_digits=9, decimal_places=2),
),
)
.order_by('-created_at')
)
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
return super().get_context_data(**kwargs)
regie_docket_list = RegieDocketListView.as_view()
class RegieDocketAddView(CreateView):
template_name = 'lingo/invoicing/manager_docket_form.html'
model = PaymentDocket
form_class = PaymentDocketForm
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
if self.regie.paymentdocket_set.filter(draft=True).exists():
raise Http404
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
return super().get_context_data(**kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if not kwargs.get('instance'):
kwargs['instance'] = self.model()
kwargs['instance'].regie = self.regie
kwargs['instance'].draft = True
kwargs['regie'] = self.regie
pmarillonnet marked this conversation as resolved Outdated

Ça correspond à quelle disjonction, ce test request.GET évalué à vrai ou non, sans chercher davantage à creuser ce que ce dictionnaire contiendrait ? Ça correspond à quels cas ?
À vue de nez on dirait qu’on cherche à faire dans def get(…): ce qu’une CreateView opérerait normalement dans def post(…):.

Ça correspond à quelle disjonction, ce test `request.GET` évalué à vrai ou non, sans chercher davantage à creuser ce que ce dictionnaire contiendrait ? Ça correspond à quels cas ? À vue de nez on dirait qu’on cherche à faire dans `def get(…):` ce qu’une CreateView opérerait normalement dans `def post(…):`.

effectivement, je voulais qu'au click sur le bouton "créer", s'il y a des trucs dans request.GET, qui viennent du listing des paiements non remis et du filtrage qui a été fait, et qu'il n'y a pas d'erreur, faire ce qui est fait normalement en POST. En gros, zapper la page de présentation du formulaire qu'on doit submit, pour éviter de remontrer à l'usager un formulaire qu'il a déjà renseigné sur la page de listing

effectivement, je voulais qu'au click sur le bouton "créer", s'il y a des trucs dans request.GET, qui viennent du listing des paiements non remis et du filtrage qui a été fait, et qu'il n'y a pas d'erreur, faire ce qui est fait normalement en POST. En gros, zapper la page de présentation du formulaire qu'on doit submit, pour éviter de remontrer à l'usager un formulaire qu'il a déjà renseigné sur la page de listing

Ah oui ok, je comprends mieux, merci.

Ah oui ok, je comprends mieux, merci.
return kwargs
def get(self, request, *args, **kwargs):
form = self.get_form_class()(
data={
'payment_types': request.GET.getlist('payment_type'),
'date_end': request.GET.get('date_end'),
}
if request.GET
else None,
**self.get_form_kwargs(),
)
if form.is_valid():
return self.form_valid(form)
return super().get(request, *args, **kwargs)
def get_success_url(self):
return reverse('lingo-manager-invoicing-regie-docket-detail', args=[self.regie.pk, self.object.pk])
regie_docket_add = RegieDocketAddView.as_view()
class RegieDocketDetailView(DetailView):
template_name = 'lingo/invoicing/manager_docket_detail.html'
model = PaymentDocket
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 self.regie.paymentdocket_set.all()
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
return super().get_context_data(**kwargs)
regie_docket_detail = RegieDocketDetailView.as_view()
class RegieDocketPaymentTypeEditView(UpdateView):
template_name = 'lingo/invoicing/manager_docket_payment_type_form.html'
model = PaymentDocket
form_class = PaymentDocketPaymentTypeForm
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 self.regie.paymentdocket_set.filter(draft=True)
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
return super().get_context_data(**kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['payment_type'] = get_object_or_404(
self.object.payment_types, pk=self.kwargs['payment_type_pk']
)
return kwargs
def get_success_url(self):
return reverse('lingo-manager-invoicing-regie-docket-detail', args=[self.regie.pk, self.object.pk])
regie_docket_payment_type_edit = RegieDocketPaymentTypeEditView.as_view()
class RegieDocketDeleteView(DeleteView):
template_name = 'lingo/manager_confirm_delete.html'
model = PaymentDocket
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 self.regie.paymentdocket_set.filter(draft=True)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.payment_set.update(docket=None)
return super().delete(request, *args, **kwargs)
def get_success_url(self):
return reverse('lingo-manager-invoicing-regie-docket-list', args=[self.regie.pk])
regie_docket_delete = RegieDocketDeleteView.as_view()
class RegieCreditListView(ListView):
template_name = 'lingo/invoicing/manager_credit_list.html'
paginate_by = 100

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,727 @@
import datetime
import decimal
import pytest
from django.utils.formats import date_format
from django.utils.timezone import localtime, now
from pyquery import PyQuery
from lingo.agendas.models import Agenda
from lingo.invoicing.models import (
AppearanceSettings,
Credit,
CreditAssignment,
CreditLine,
Invoice,
Payment,
PaymentType,
Refund,
Regie,
)
from tests.utils import login
pytestmark = pytest.mark.django_db
def test_regie_credits(app, admin_user):
regie = Regie.objects.create(label='Foo')
Agenda.objects.create(label='Agenda A', regie=regie)
Agenda.objects.create(label='Agenda B', regie=regie)
PaymentType.create_defaults(regie)
invoice = 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,
)
credit1 = Credit.objects.create(
regie=regie,
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
)
credit1.set_number()
credit1.save()
credit2 = Credit.objects.create(
regie=regie,
payer_external_id='payer:2',
payer_first_name='First2',
payer_last_name='Name2',
payer_address='42 rue des kangourous\n99999 Kangourou Ville',
)
credit2.set_number()
credit2.save()
credit3 = Credit.objects.create(
regie=regie,
payer_external_id='payer:3',
payer_first_name='First3',
payer_last_name='Name3',
payer_address='43 rue des kangourous\n99999 Kangourou Ville',
)
credit3.set_number()
credit3.save()
CreditLine.objects.create(
slug='event-a-foo-bar',
event_date=datetime.date(2022, 9, 1),
credit=credit1,
quantity=1.2,
unit_amount=1,
label='Event A',
description='A description',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
event_slug='agenda-a@event-a',
agenda_slug='agenda-a',
activity_label='Agenda A',
)
CreditLine.objects.create(
slug='event-b-foo-bar',
event_date=datetime.date(2022, 9, 2),
credit=credit1,
quantity=1,
unit_amount=2,
label='Event B',
user_external_id='user:2',
user_first_name='User2',
user_last_name='Name2',
event_slug='agenda-b@event-b',
agenda_slug='agenda-b',
activity_label='Agenda B',
)
CreditLine.objects.create(
slug='event-a-foo-bar',
event_date=datetime.date(2022, 9, 3),
credit=credit1,
quantity=1,
unit_amount=3,
label='Event A',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
event_slug='agenda-a@event-a',
agenda_slug='agenda-a',
activity_label='Agenda A',
)
payment1 = Payment.objects.create(
regie=regie,
amount=1,
payment_type=PaymentType.objects.get(regie=regie, slug='credit'),
)
payment1.set_number()
payment1.save()
credit_assignment1 = CreditAssignment.objects.create(
invoice=invoice,
payment=payment1,
credit=credit1,
amount=1,
)
refund = Refund.objects.create(
regie=regie,
amount=5.2,
)
refund.set_number()
refund.save()
credit_assignment2 = CreditAssignment.objects.create(
refund=refund,
credit=credit1,
amount=5.2,
)
credit1.refresh_from_db()
assert credit1.remaining_amount == 0
assert credit1.assigned_amount == decimal.Decimal('6.2')
CreditLine.objects.create(
slug='agenda-a@event-aa',
event_date=datetime.date(2022, 9, 1),
credit=credit2,
quantity=1,
unit_amount=1,
label='Event AA',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
event_slug='agenda-a@event-aa',
agenda_slug='agenda-a',
activity_label='Agenda A',
)
CreditAssignment.objects.create(
invoice=invoice,
credit=credit2,
amount=0.5,
)
credit2.refresh_from_db()
assert credit2.remaining_amount == 0.5
assert credit2.assigned_amount == 0.5
CreditLine.objects.create(
slug='injected',
event_date=datetime.date(2022, 9, 1),
credit=credit3,
quantity=1,
unit_amount=1,
label='Event A',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
credit3.refresh_from_db()
assert credit3.remaining_amount == 1
assert credit3.assigned_amount == 0
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/credits/' % regie.pk)
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % credit1.pk
).text() == 'Assigned Credit A%02d-%s-0000001 dated %s for First1 Name1, amount 6.20€ - download' % (
regie.pk,
credit1.created_at.strftime('%y-%m'),
credit1.created_at.strftime('%d/%m/%Y'),
)
assert len(resp.pyquery('tr[data-invoicing-element-id="%s"] a' % credit1.pk)) == 2
lines_url = resp.pyquery('tr[data-invoicing-element-id="%s"]' % credit1.pk).attr(
'data-invoicing-element-lines-url'
)
assert lines_url == '/manage/invoicing/ajax/regie/%s/credit/%s/lines/' % (
regie.pk,
credit1.pk,
)
lines_resp = app.get(lines_url)
assert len(lines_resp.pyquery('tr')) == 14
assert [PyQuery(tr).text() for tr in lines_resp.pyquery('tr')] == [
'User1 Name1',
'Description\nAmount\nQuantity\nSubtotal',
'Agenda A',
'Event A\nA description\n1.00€\n1.2\n1.20€',
'Event A\n3.00€\n1\n3.00€',
'User2 Name2',
'Description\nAmount\nQuantity\nSubtotal',
'Agenda B',
'Event B\n2.00€\n1\n2.00€',
'Assignments',
'Payment\nDate\nAmount',
'R%02d-%s-0000001\n%s\n1.00€'
% (
regie.pk,
payment1.created_at.strftime('%y-%m'),
date_format(localtime(credit_assignment1.created_at), 'DATETIME_FORMAT'),
),
'V%02d-%s-0000001 (Refund)\n%s\n5.20€'
% (
regie.pk,
refund.created_at.strftime('%y-%m'),
date_format(localtime(credit_assignment2.created_at), 'DATETIME_FORMAT'),
),
'Assigned amount: 6.20€',
]
assert [PyQuery(a).attr('href') for a in lines_resp.pyquery('tr a')] == [
'/manage/invoicing/regie/%s/payments/?number=R%s-%s-0000001'
% (
regie.pk,
regie.pk,
payment1.created_at.strftime('%y-%m'),
),
'/manage/invoicing/regie/%s/refunds/?number=V%s-%s-0000001'
% (
regie.pk,
regie.pk,
refund.created_at.strftime('%y-%m'),
),
]
assert len(resp.pyquery('tr[data-invoicing-element-id="%s"] a' % credit2.pk)) == 2
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % credit2.pk
).text() == 'Partially assigned Credit A%02d-%s-0000002 dated %s for First2 Name2, amount 1.00€ - download' % (
regie.pk,
credit2.created_at.strftime('%y-%m'),
credit2.created_at.strftime('%d/%m/%Y'),
)
lines_url = resp.pyquery('tr[data-invoicing-element-id="%s"]' % credit2.pk).attr(
'data-invoicing-element-lines-url'
)
assert lines_url == '/manage/invoicing/ajax/regie/%s/credit/%s/lines/' % (
regie.pk,
credit2.pk,
)
lines_resp = app.get(lines_url)
assert len(lines_resp.pyquery('tr')) == 9
assert [PyQuery(tr).text() for tr in lines_resp.pyquery('tr')] == [
'User1 Name1',
'Description\nAmount\nQuantity\nSubtotal',
'Agenda A',
'Event AA\n1.00€\n1\n1.00€',
'Assignments',
'Payment\nDate\nAmount',
'Pending...\n0.50€',
'Assigned amount: 0.50€',
'Remaining amount to assign: 0.50€',
]
assert [PyQuery(a).attr('href') for a in lines_resp.pyquery('tr a')] == []
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % credit3.pk
).text() == 'Credit A%02d-%s-0000003 dated %s for First3 Name3, amount 1.00€ - download' % (
regie.pk,
credit3.created_at.strftime('%y-%m'),
credit3.created_at.strftime('%d/%m/%Y'),
)
assert len(resp.pyquery('tr[data-invoicing-element-id="%s"] a' % credit3.pk)) == 2
lines_url = resp.pyquery('tr[data-invoicing-element-id="%s"]' % credit3.pk).attr(
'data-invoicing-element-lines-url'
)
assert lines_url == '/manage/invoicing/ajax/regie/%s/credit/%s/lines/' % (
regie.pk,
credit3.pk,
)
lines_resp = app.get(lines_url)
assert len(lines_resp.pyquery('tr')) == 7
assert [PyQuery(tr).text() for tr in lines_resp.pyquery('tr')] == [
'User1 Name1',
'Description\nAmount\nQuantity\nSubtotal',
'Event A\n1.00€\n1\n1.00€',
'Assignments',
'Payment\nDate\nAmount',
'No assignments for this credit',
'Remaining amount to assign: 1.00€',
]
# test filters
today = now().date()
tomorrow = today + datetime.timedelta(days=1)
yesterday = today - datetime.timedelta(days=1)
params = [
({'number': credit1.formatted_number}, 1),
({'number': credit1.created_at.strftime('%y-%m')}, 3),
({'created_at_after': today.strftime('%Y-%m-%d')}, 3),
({'created_at_after': tomorrow.strftime('%Y-%m-%d')}, 0),
({'created_at_before': yesterday.strftime('%Y-%m-%d')}, 0),
({'created_at_before': today.strftime('%Y-%m-%d')}, 3),
({'payment_number': payment1.formatted_number}, 1),
({'payment_number': payment1.created_at.strftime('%y-%m')}, 1),
({'payer_external_id': 'payer:1'}, 1),
({'payer_external_id': 'payer:2'}, 1),
({'payer_first_name': 'first'}, 3),
({'payer_first_name': 'first1'}, 1),
({'payer_last_name': 'name'}, 3),
({'payer_last_name': 'name1'}, 1),
({'user_external_id': 'user:1'}, 3),
({'user_external_id': 'user:2'}, 1),
({'user_first_name': 'user'}, 3),
({'user_first_name': 'user2'}, 1),
({'user_last_name': 'name'}, 3),
({'user_last_name': 'name1'}, 3),
(
{
'total_amount_min': '1',
'total_amount_min_lookup': 'gt',
},
1,
),
(
{
'total_amount_min': '1',
'total_amount_min_lookup': 'gte',
},
3,
),
(
{
'total_amount_max': '6.2',
'total_amount_max_lookup': 'lt',
},
2,
),
(
{
'total_amount_max': '6.2',
'total_amount_max_lookup': 'lte',
},
3,
),
({'assigned': 'yes'}, 1),
({'assigned': 'partially'}, 1),
({'assigned': 'no'}, 1),
({'agenda': 'agenda-a'}, 2),
({'agenda': 'agenda-b'}, 1),
({'event': 'agenda-a@event-a'}, 1),
({'event': 'agenda-a@event-aa'}, 1),
({'event': 'agenda-b@event-b'}, 1),
]
for param, result in params:
resp = app.get(
'/manage/invoicing/regie/%s/credits/' % regie.pk,
params=param,
)
assert len(resp.pyquery('tr.credit')) == result
def test_regie_credit_pdf(app, admin_user):
regie = Regie.objects.create(label='Foo', invoice_main_colour='#9141ac')
credit = Credit.objects.create(
label='Credit from 01/09/2022',
regie=regie,
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
)
credit.set_number()
credit.save()
CreditLine.objects.create(
event_date=datetime.date(2022, 9, 1),
credit=credit,
quantity=1.2,
unit_amount=1,
label='Label 11',
description='A description',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
event_slug='agenda-a@event-a',
agenda_slug='agenda-a',
activity_label='Agenda A',
)
CreditLine.objects.create(
event_date=datetime.date(2022, 9, 2),
credit=credit,
quantity=1,
unit_amount=2,
label='Label 12',
user_external_id='user:2',
user_first_name='User2',
user_last_name='Name2',
event_slug='agenda-a@event-a',
agenda_slug='agenda-a',
activity_label='Agenda A',
)
CreditLine.objects.create(
event_date=datetime.date(2022, 9, 3),
credit=credit,
quantity=1,
unit_amount=3,
label='Label 13',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
event_slug='agenda-b@event-b',
agenda_slug='agenda-b',
activity_label='Agenda B',
)
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/credit/%s/pdf/?html' % (regie.pk, credit.pk))
assert 'color: #9141ac;' in resp
assert resp.pyquery('#document-label').text() == 'Credit from 01/09/2022'
assert resp.pyquery('#regie-label').text() == 'Foo'
assert resp.pyquery('address#to').text() == 'First1 Name1\n41 rue des kangourous\n99999 Kangourou Ville'
assert resp.pyquery('dl#informations').text() == 'Credit number:\nA%02d-%s-0000001\nDate:\n%s' % (
regie.pk,
credit.created_at.strftime('%y-%m'),
date_format(localtime(credit.created_at), 'DATE_FORMAT'),
)
assert [PyQuery(tr).text() for tr in resp.pyquery('table#lines tr')] == [
'User1 Name1',
'Services\nDetails\nUA\nQTY\nTA',
'Label 11\nAgenda A\n\nA description\n1.00€\n1.2\n1.20€',
'Label 13\nAgenda B\n\n3.00€\n1\n3.00€',
'User2 Name2',
'Services\nDetails\nUA\nQTY\nTA',
'Label 12\nAgenda A\n\n2.00€\n1\n2.00€',
'Total amount:\n6.20€',
]
app.get('/manage/invoicing/regie/%s/credit/%s/pdf/?html' % (0, credit.pk), status=404)
app.get('/manage/invoicing/regie/%s/credit/%s/pdf/?html' % (regie.pk, 0), status=404)
other_regie = Regie.objects.create(label='Foo')
app.get('/manage/invoicing/regie/%s/credit/%s/pdf/?html' % (other_regie.pk, credit.pk), status=404)
appearance_settings = AppearanceSettings.singleton()
appearance_settings.address = '<p>Foo bar<br>Streetname</p>'
appearance_settings.extra_info = '<p>Opening hours...</p>'
appearance_settings.save()
resp = app.get('/manage/invoicing/regie/%s/credit/%s/pdf/?html' % (regie.pk, credit.pk))
assert appearance_settings.address in resp.text
assert appearance_settings.extra_info in resp.text
def test_regie_refunds(app, admin_user):
regie = Regie.objects.create(label='Foo')
Agenda.objects.create(label='Agenda A', regie=regie)
Agenda.objects.create(label='Agenda B', regie=regie)
PaymentType.create_defaults(regie)
invoice = 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,
)
credit1 = Credit.objects.create(
regie=regie,
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
)
credit1.set_number()
credit1.save()
credit2 = Credit.objects.create(
regie=regie,
payer_external_id='payer:2',
payer_first_name='First2',
payer_last_name='Name2',
payer_address='42 rue des kangourous\n99999 Kangourou Ville',
)
credit2.set_number()
credit2.save()
credit3 = Credit.objects.create(
regie=regie,
payer_external_id='payer:3',
payer_first_name='First3',
payer_last_name='Name3',
payer_address='43 rue des kangourous\n99999 Kangourou Ville',
)
credit3.set_number()
credit3.save()
CreditLine.objects.create(
slug='event-a-foo-bar',
event_date=datetime.date(2022, 9, 1),
credit=credit1,
quantity=1.2,
unit_amount=1,
label='Event A',
description='A description',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
CreditLine.objects.create(
slug='event-b-foo-bar',
event_date=datetime.date(2022, 9, 2),
credit=credit1,
quantity=1,
unit_amount=2,
label='Event B',
user_external_id='user:2',
user_first_name='User2',
user_last_name='Name2',
)
CreditLine.objects.create(
slug='event-a-foo-bar',
event_date=datetime.date(2022, 9, 3),
credit=credit1,
quantity=1,
unit_amount=3,
label='Event A',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
refund1 = Refund.objects.create(
regie=regie,
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
amount=6.2,
)
refund1.set_number()
refund1.save()
CreditAssignment.objects.create(
refund=refund1,
credit=credit1,
amount=6.2,
)
credit1.refresh_from_db()
assert credit1.remaining_amount == 0
assert credit1.assigned_amount == decimal.Decimal('6.2')
CreditLine.objects.create(
slug='agenda-a@event-aa',
event_date=datetime.date(2022, 9, 1),
credit=credit2,
quantity=1,
unit_amount=1,
label='Event AA',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
payment2 = Payment.objects.create(
regie=regie,
amount=0.5,
payment_type=PaymentType.objects.get(regie=regie, slug='credit'),
)
payment2.set_number()
payment2.save()
CreditAssignment.objects.create(
invoice=invoice,
payment=payment2,
credit=credit2,
amount=0.5,
)
refund2 = Refund.objects.create(
regie=regie,
payer_external_id='payer:2',
payer_first_name='First2',
payer_last_name='Name2',
payer_address='42 rue des kangourous\n99999 Kangourou Ville',
amount=0.5,
)
refund2.set_number()
refund2.save()
CreditAssignment.objects.create(
refund=refund2,
credit=credit2,
amount=0.5,
)
credit2.refresh_from_db()
assert credit2.remaining_amount == 0
assert credit2.assigned_amount == 1
CreditLine.objects.create(
slug='injected',
event_date=datetime.date(2022, 9, 1),
credit=credit3,
quantity=1,
unit_amount=1,
label='Event A',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
refund3 = Refund.objects.create(
regie=regie,
payer_external_id='payer:3',
payer_first_name='First3',
payer_last_name='Name3',
payer_address='43 rue des kangourous\n99999 Kangourou Ville',
amount=1,
)
refund3.set_number()
refund3.save()
CreditAssignment.objects.create(
refund=refund3,
credit=credit3,
amount=1,
)
credit3.refresh_from_db()
assert credit3.remaining_amount == 0
assert credit3.assigned_amount == 1
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/refunds/' % regie.pk)
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % refund1.pk
).text() == 'Refund V%02d-%s-0000001 dated %s for First1 Name1, amount 6.20€' % (
regie.pk,
refund1.created_at.strftime('%y-%m'),
refund1.created_at.strftime('%d/%m/%Y'),
)
assert [
PyQuery(tr).text() for tr in resp.pyquery('tr[data-related-invoicing-element-id="%s"]' % refund1.pk)
] == [
'Credit\nDate\nCredit amount\nRefund amount',
'A%02d-%s-0000001\n%s\n6.20€\n6.20€'
% (
regie.pk,
credit1.created_at.strftime('%y-%m'),
credit1.created_at.strftime('%d/%m/%Y'),
),
]
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % refund2.pk
).text() == 'Refund V%02d-%s-0000002 dated %s for First2 Name2, amount 0.50€' % (
regie.pk,
refund2.created_at.strftime('%y-%m'),
refund2.created_at.strftime('%d/%m/%Y'),
)
assert [
PyQuery(tr).text() for tr in resp.pyquery('tr[data-related-invoicing-element-id="%s"]' % refund2.pk)
] == [
'Credit\nDate\nCredit amount\nRefund amount',
'A%02d-%s-0000002\n%s\n1.00€\n0.50€'
% (
regie.pk,
credit2.created_at.strftime('%y-%m'),
credit2.created_at.strftime('%d/%m/%Y'),
),
]
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % refund3.pk
).text() == 'Refund V%02d-%s-0000003 dated %s for First3 Name3, amount 1.00€' % (
regie.pk,
refund3.created_at.strftime('%y-%m'),
refund3.created_at.strftime('%d/%m/%Y'),
)
assert [
PyQuery(tr).text() for tr in resp.pyquery('tr[data-related-invoicing-element-id="%s"]' % refund3.pk)
] == [
'Credit\nDate\nCredit amount\nRefund amount',
'A%02d-%s-0000003\n%s\n1.00€\n1.00€'
% (
regie.pk,
credit3.created_at.strftime('%y-%m'),
credit3.created_at.strftime('%d/%m/%Y'),
),
]
# test filters
today = now().date()
tomorrow = today + datetime.timedelta(days=1)
yesterday = today - datetime.timedelta(days=1)
params = [
({'number': refund1.formatted_number}, 1),
({'number': refund1.created_at.strftime('%y-%m')}, 3),
({'created_at_after': today.strftime('%Y-%m-%d')}, 3),
({'created_at_after': tomorrow.strftime('%Y-%m-%d')}, 0),
({'created_at_before': yesterday.strftime('%Y-%m-%d')}, 0),
({'created_at_before': today.strftime('%Y-%m-%d')}, 3),
({'credit_number': credit1.formatted_number}, 1),
({'credit_number': credit1.created_at.strftime('%y-%m')}, 3),
({'payer_external_id': 'payer:1'}, 1),
({'payer_external_id': 'payer:2'}, 1),
({'payer_first_name': 'first'}, 3),
({'payer_first_name': 'first1'}, 1),
({'payer_last_name': 'name'}, 3),
({'payer_last_name': 'name1'}, 1),
(
{
'amount_min': '1',
'amount_min_lookup': 'gt',
},
1,
),
(
{
'amount_min': '1',
'amount_min_lookup': 'gte',
},
2,
),
(
{
'amount_max': '6.2',
'amount_max_lookup': 'lt',
},
2,
),
(
{
'amount_max': '6.2',
'amount_max_lookup': 'lte',
},
3,
),
]
for param, result in params:
resp = app.get(
'/manage/invoicing/regie/%s/refunds/' % regie.pk,
params=param,
)
assert len(resp.pyquery('tr.refund')) == result

View File

@ -0,0 +1,544 @@
import datetime
import decimal
import pytest
from django.utils.timezone import now
from pyquery import PyQuery
from lingo.invoicing.models import Payment, PaymentDocket, PaymentType, Regie
from tests.utils import login
pytestmark = pytest.mark.django_db
def test_regie_payments_outside_dockets(app, admin_user):
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
docket = PaymentDocket.objects.create(regie=regie, date_end=now().date(), draft=True)
payment1 = Payment.objects.create(
regie=regie,
amount=35,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
)
payment1.created_at = now() - datetime.timedelta(days=2)
payment1.set_number()
payment1.save()
payment2 = 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',
)
payment2.created_at = now() - datetime.timedelta(days=1)
payment2.set_number()
payment2.save()
payment3 = Payment.objects.create(
regie=regie,
amount=2,
payment_type=PaymentType.objects.get(regie=regie, slug='creditcard'),
payer_external_id='payer:3',
payer_first_name='First3',
payer_last_name='Name3',
)
payment3.created_at = now() - datetime.timedelta(days=1)
payment3.set_number()
payment3.save()
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',
)
payment4.set_number()
payment4.save()
payment5 = 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',
cancelled_at=now(),
)
payment5.set_number()
payment5.save()
payment6 = 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',
docket=docket,
)
payment6.set_number()
payment6.save()
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/' % regie.pk)
resp = resp.click('Dockets')
assert [PyQuery(tr).text() for tr in resp.pyquery('tr')] == [
'Payment R%02d-%s-0000003 dated %s from First3 Name3, amount 2.00€ (Credit card)'
% (regie.pk, payment3.created_at.strftime('%y-%m'), payment3.created_at.strftime('%d/%m/%Y')),
'Payment R%02d-%s-0000002 dated %s from First1 Name1, amount 55.00€ (Check)'
% (regie.pk, payment2.created_at.strftime('%y-%m'), payment2.created_at.strftime('%d/%m/%Y')),
'Payment R%02d-%s-0000001 dated %s from First1 Name1, amount 35.00€ (Cash)'
% (regie.pk, payment1.created_at.strftime('%y-%m'), payment1.created_at.strftime('%d/%m/%Y')),
]
resp.form['date_end'] = now().date() + datetime.timedelta(days=1)
resp = resp.form.submit()
assert [PyQuery(tr).text() for tr in resp.pyquery('tr')] == [
'Payment R%02d-%s-0000004 dated %s from First3 Name3, amount 2.00€ (Check)'
% (regie.pk, payment4.created_at.strftime('%y-%m'), payment4.created_at.strftime('%d/%m/%Y')),
'Payment R%02d-%s-0000003 dated %s from First3 Name3, amount 2.00€ (Credit card)'
% (regie.pk, payment3.created_at.strftime('%y-%m'), payment3.created_at.strftime('%d/%m/%Y')),
'Payment R%02d-%s-0000002 dated %s from First1 Name1, amount 55.00€ (Check)'
% (regie.pk, payment2.created_at.strftime('%y-%m'), payment2.created_at.strftime('%d/%m/%Y')),
'Payment R%02d-%s-0000001 dated %s from First1 Name1, amount 35.00€ (Cash)'
% (regie.pk, payment1.created_at.strftime('%y-%m'), payment1.created_at.strftime('%d/%m/%Y')),
]
resp = app.get('/manage/invoicing/regie/%s/dockets/payments/' % regie.pk)
resp.form['payment_type'] = [
PaymentType.objects.get(slug='cash').pk,
PaymentType.objects.get(slug='check').pk,
]
resp.form['date_end'] = now().date() + datetime.timedelta(days=1)
resp = resp.form.submit()
assert [PyQuery(tr).text() for tr in resp.pyquery('tr')] == [
'Payment R%02d-%s-0000004 dated %s from First3 Name3, amount 2.00€ (Check)'
% (regie.pk, payment4.created_at.strftime('%y-%m'), payment4.created_at.strftime('%d/%m/%Y')),
'Payment R%02d-%s-0000002 dated %s from First1 Name1, amount 55.00€ (Check)'
% (regie.pk, payment2.created_at.strftime('%y-%m'), payment2.created_at.strftime('%d/%m/%Y')),
'Payment R%02d-%s-0000001 dated %s from First1 Name1, amount 35.00€ (Cash)'
% (regie.pk, payment1.created_at.strftime('%y-%m'), payment1.created_at.strftime('%d/%m/%Y')),
]
def test_regie_docket_list(app, admin_user):
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
docket1 = PaymentDocket.objects.create(
regie=regie, date_end=now().date() + datetime.timedelta(days=1), draft=False
)
docket1.set_number()
docket1.save()
docket2 = PaymentDocket.objects.create(
regie=regie, date_end=now().date() + datetime.timedelta(days=1), draft=True
)
Payment.objects.create(
regie=regie,
amount=decimal.Decimal('35.5'),
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
docket=docket1,
)
Payment.objects.create(
regie=regie,
amount=42,
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
docket=docket1,
)
Payment.objects.create(
regie=regie,
amount=43,
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
docket=docket2,
)
Payment.objects.create(
regie=regie,
amount=decimal.Decimal('44.5'),
payment_type=PaymentType.objects.get(regie=regie, slug='creditcard'),
docket=docket2,
cancelled_at=now(),
)
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/' % regie.pk)
resp = resp.click('Dockets')
resp = resp.click('Dockets')
assert [PyQuery(tr).text() for tr in resp.pyquery('tr')] == [
'Number\nPayment types\nNumber of payments\nEnd date',
'TEMPORARY-%s\n1 (43.00€) 1 (44.50€)\n%s' % (docket2.pk, docket2.date_end.strftime('%d/%m/%Y')),
'B%02d-%s-0000001\n2 (77.50€)\n%s'
% (regie.pk, docket1.created_at.strftime('%y-%m'), docket1.date_end.strftime('%d/%m/%Y')),
]
def test_regie_docket_add(app, admin_user):
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
docket = PaymentDocket.objects.create(regie=regie, date_end=now().date(), draft=False)
payment1 = Payment.objects.create(
regie=regie,
amount=35,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
)
payment1.created_at = now() - datetime.timedelta(days=2)
payment1.set_number()
payment1.save()
payment2 = 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',
)
payment2.created_at = now() - datetime.timedelta(days=1)
payment2.set_number()
payment2.save()
payment3 = Payment.objects.create(
regie=regie,
amount=2,
payment_type=PaymentType.objects.get(regie=regie, slug='creditcard'),
payer_external_id='payer:3',
payer_first_name='First3',
payer_last_name='Name3',
)
payment3.created_at = now() - datetime.timedelta(days=1)
payment3.set_number()
payment3.save()
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',
)
payment4.set_number()
payment4.save()
payment5 = 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',
cancelled_at=now(),
)
payment5.set_number()
payment5.save()
payment6 = 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',
docket=docket,
)
payment6.set_number()
payment6.save()
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/dockets/payments/' % regie.pk)
resp = resp.click('New docket')
docket = PaymentDocket.objects.latest('pk')
assert resp.location.endswith('/manage/invoicing/regie/%s/docket/%s/' % (regie.pk, docket.pk))
assert docket.regie == regie
assert docket.draft is True
assert docket.date_end == now().date()
assert list(docket.payment_types.all()) == list(PaymentType.objects.all())
assert list(docket.payment_set.all().order_by('-pk')) == [payment3, payment2, payment1]
resp = app.get('/manage/invoicing/regie/%s/dockets/payments/' % regie.pk)
assert 'New docket' not in resp
app.get('/manage/invoicing/regie/%s/docket/add/' % regie.pk, status=404)
Payment.objects.filter(docket=docket).update(docket=None)
docket.delete()
resp = app.get('/manage/invoicing/regie/%s/dockets/payments/' % regie.pk)
resp = resp.click('New docket')
docket = PaymentDocket.objects.latest('pk')
assert resp.location.endswith('/manage/invoicing/regie/%s/docket/%s/' % (regie.pk, docket.pk))
assert docket.regie == regie
assert docket.draft is True
assert docket.date_end == now().date()
assert list(docket.payment_types.all()) == list(PaymentType.objects.all())
assert list(docket.payment_set.all().order_by('-pk')) == [payment3, payment2, payment1]
resp = app.get('/manage/invoicing/regie/%s/dockets/payments/' % regie.pk)
assert 'New docket' not in resp
Payment.objects.filter(docket=docket).update(docket=None)
docket.delete()
resp = app.get('/manage/invoicing/regie/%s/dockets/payments/' % regie.pk)
resp.form['payment_type'] = [p.pk for p in PaymentType.objects.all()]
resp.form['date_end'] = now().date() + datetime.timedelta(days=1)
resp = resp.form.submit()
resp = resp.click('New docket')
docket = PaymentDocket.objects.latest('pk')
assert resp.location.endswith('/manage/invoicing/regie/%s/docket/%s/' % (regie.pk, docket.pk))
assert docket.regie == regie
assert docket.draft is True
assert docket.date_end == now().date() + datetime.timedelta(days=1)
assert list(docket.payment_types.all()) == list(PaymentType.objects.all())
assert list(docket.payment_set.all().order_by('-pk')) == [payment4, payment3, payment2, payment1]
resp = app.get('/manage/invoicing/regie/%s/dockets/payments/' % regie.pk)
assert 'New docket' not in resp
Payment.objects.filter(docket=docket).update(docket=None)
docket.delete()
resp = app.get('/manage/invoicing/regie/%s/dockets/payments/' % regie.pk)
resp.form['payment_type'] = [
PaymentType.objects.get(slug='cash').pk,
PaymentType.objects.get(slug='check').pk,
]
resp.form['date_end'] = now().date() + datetime.timedelta(days=1)
resp = resp.form.submit()
resp = resp.click('New docket')
docket = PaymentDocket.objects.latest('pk')
assert resp.location.endswith('/manage/invoicing/regie/%s/docket/%s/' % (regie.pk, docket.pk))
assert docket.regie == regie
assert docket.draft is True
assert docket.date_end == now().date() + datetime.timedelta(days=1)
assert list(docket.payment_types.all()) == list(PaymentType.objects.filter(slug__in=['cash', 'check']))
assert list(docket.payment_set.all().order_by('-pk')) == [payment4, payment2, payment1]
docket.draft = False
docket.save()
resp = app.get('/manage/invoicing/regie/%s/dockets/payments/' % regie.pk)
assert 'New docket' in resp
app.get('/manage/invoicing/regie/%s/docket/add/' % regie.pk, status=302)
def test_regie_docket_detail(app, admin_user):
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
docket = PaymentDocket.objects.create(
regie=regie,
date_end=now().date() + datetime.timedelta(days=1),
draft=False,
payment_types_info={
'cash': 'foo bar\nblah',
'check': 'foo bar',
},
)
docket.payment_types.set(PaymentType.objects.all())
docket.set_number()
docket.save()
payment1 = Payment.objects.create(
regie=regie,
amount=35,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
docket=docket,
)
payment1.set_number()
payment1.save()
payment2 = Payment.objects.create(
regie=regie,
amount=42,
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
payer_external_id='payer:2',
payer_first_name='First2',
payer_last_name='Name2',
docket=docket,
)
payment2.set_number()
payment2.save()
payment3 = Payment.objects.create(
regie=regie,
amount=43,
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
payer_external_id='payer:3',
payer_first_name='First3',
payer_last_name='Name3',
docket=docket,
cancelled_at=now(),
)
payment3.set_number()
payment3.save()
payment4 = Payment.objects.create(
regie=regie,
amount=23,
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
payer_external_id='payer:3',
payer_first_name='First3',
payer_last_name='Name3',
docket=docket,
)
payment4.set_number()
payment4.save()
Payment.objects.create(
regie=regie,
amount=44,
payment_type=PaymentType.objects.get(regie=regie, slug='creditcard'),
)
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/' % regie.pk)
resp = resp.click('Dockets')
resp = resp.click('Dockets')
resp = resp.click(docket.formatted_number)
assert [PyQuery(h3).text() for h3 in resp.pyquery('h3')] == [
'Cash',
'Check',
'Cancelled payments',
]
assert len(resp.pyquery('table')) == 3
assert [PyQuery(tr).text() for tr in PyQuery(resp.pyquery('table')[0]).find('tr')] == [
'Payment R%02d-%s-0000001 dated %s from First1 Name1, amount 35.00€ (Cash)'
% (regie.pk, payment1.created_at.strftime('%y-%m'), payment1.created_at.strftime('%d/%m/%Y')),
]
assert [PyQuery(tr).text() for tr in PyQuery(resp.pyquery('table')[1]).find('tr')] == [
'Payment R%02d-%s-0000004 dated %s from First3 Name3, amount 23.00€ (Check)'
% (regie.pk, payment4.created_at.strftime('%y-%m'), payment4.created_at.strftime('%d/%m/%Y')),
'Payment R%02d-%s-0000002 dated %s from First2 Name2, amount 42.00€ (Check)'
% (regie.pk, payment2.created_at.strftime('%y-%m'), payment2.created_at.strftime('%d/%m/%Y')),
]
assert [PyQuery(tr).text() for tr in PyQuery(resp.pyquery('table')[2]).find('tr')] == [
'Payment R%02d-%s-0000003 dated %s from First3 Name3, amount 43.00€ (Check)'
% (regie.pk, payment3.created_at.strftime('%y-%m'), payment3.created_at.strftime('%d/%m/%Y')),
]
assert len(resp.pyquery('p')) == 3
assert (
PyQuery(resp.pyquery('p')[0]).text()
== 'Number of payments: 1\nTotal amount: 35.00€\nAdditionnal information:\nfoo bar\nblah'
)
assert (
PyQuery(resp.pyquery('p')[1]).text()
== 'Number of payments: 2\nTotal amount: 65.00€\nAdditionnal information:\nfoo bar'
)
assert PyQuery(resp.pyquery('p')[2]).text() == 'Number of payments: 1\nTotal amount: 43.00€'
def test_regie_docket_payment_type_edit(app, admin_user):
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
docket = PaymentDocket.objects.create(
regie=regie,
date_end=now().date() + datetime.timedelta(days=1),
draft=False,
payment_types_info={
'cash': 'foo bar\nblah',
'check': 'foo bar',
},
)
docket.payment_types.set(PaymentType.objects.all())
docket.set_number()
docket.save()
Payment.objects.create(
regie=regie,
amount=35,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
docket=docket,
)
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/docket/%s/' % (regie.pk, docket.pk))
assert (
'/manage/invoicing/regie/%s/docket/%s/payment-type/%s/'
% (regie.pk, docket.pk, PaymentType.objects.get(regie=regie, slug='cash').pk)
not in resp
)
app.get(
'/manage/invoicing/regie/%s/docket/%s/payment-type/%s/'
% (regie.pk, docket.pk, PaymentType.objects.get(regie=regie, slug='cash').pk),
status=404,
)
docket.draft = True
docket.save()
resp = app.get('/manage/invoicing/regie/%s/docket/%s/' % (regie.pk, docket.pk))
resp = resp.click(
href='/manage/invoicing/regie/%s/docket/%s/payment-type/%s/'
% (regie.pk, docket.pk, PaymentType.objects.get(regie=regie, slug='cash').pk)
)
resp.form['additionnal_information'].value == 'foo bar\nblah'
resp.form['additionnal_information'] = 'baz'
resp = resp.form.submit()
assert resp.location.endswith('/manage/invoicing/regie/%s/docket/%s/' % (regie.pk, docket.pk))
docket.refresh_from_db()
assert docket.payment_types_info == {
'cash': 'baz',
'check': 'foo bar',
}
def test_regie_docket_delete(app, admin_user):
regie = Regie.objects.create(label='Foo')
PaymentType.create_defaults(regie)
docket = PaymentDocket.objects.create(
regie=regie, date_end=now().date() + datetime.timedelta(days=1), draft=False
)
docket.set_number()
docket.save()
Payment.objects.create(
regie=regie,
amount=35,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
docket=docket,
)
Payment.objects.create(
regie=regie,
amount=42,
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
docket=docket,
)
Payment.objects.create(
regie=regie,
amount=43,
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
docket=docket,
cancelled_at=now(),
)
Payment.objects.create(
regie=regie,
amount=23,
payment_type=PaymentType.objects.get(regie=regie, slug='check'),
docket=docket,
)
Payment.objects.create(
regie=regie,
amount=44,
payment_type=PaymentType.objects.get(regie=regie, slug='creditcard'),
)
assert Payment.objects.filter(docket__isnull=False).count() == 4
assert Payment.objects.count() == 5
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/docket/%s/' % (regie.pk, docket.pk))
assert 'Delete' not in resp
app.get('/manage/invoicing/regie/%s/docket/%s/delete/' % (regie.pk, docket.pk), status=404)
docket.draft = True
docket.save()
resp = app.get('/manage/invoicing/regie/%s/docket/%s/' % (regie.pk, docket.pk))
resp = resp.click('Delete')
resp = resp.form.submit()
assert resp.location.endswith('/manage/invoicing/regie/%s/dockets/' % regie.pk)
assert PaymentDocket.objects.filter(pk=docket.pk).exists() is False
assert Payment.objects.filter(docket__isnull=False).count() == 0
assert Payment.objects.count() == 5

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,650 @@
import datetime
import pytest
from django.utils.timezone import now
from pyquery import PyQuery
from lingo.agendas.models import Agenda
from lingo.invoicing.models import (
Invoice,
InvoiceLine,
InvoiceLinePayment,
Payment,
PaymentCancellationReason,
PaymentDocket,
PaymentType,
Regie,
)
from tests.utils import login
pytestmark = pytest.mark.django_db
def test_regie_payments(app, admin_user):
regie = Regie.objects.create(label='Foo')
Agenda.objects.create(label='Agenda A', regie=regie)
Agenda.objects.create(label='Agenda B', regie=regie)
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()
invoice3 = 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:3',
payer_first_name='First3',
payer_last_name='Name3',
payer_address='43 rue des kangourous\n99999 Kangourou Ville',
payer_demat=False,
payer_direct_debit=True,
)
invoice3.set_number()
invoice3.save()
invoice_line1 = InvoiceLine.objects.create(
slug='event-a-foo-bar',
label='Event A',
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',
details={
'agenda': 'agenda-a',
'primary_event': 'event-a',
},
event_slug='agenda-a@event-a',
agenda_slug='agenda-a',
activity_label='Agenda A',
)
invoice_line2 = InvoiceLine.objects.create(
slug='agenda-b@event-b', # non recurring event
label='Event B',
event_date=datetime.date(2022, 9, 1),
invoice=invoice2,
quantity=1,
unit_amount=50,
event_slug='agenda-b@event-b',
agenda_slug='agenda-b',
activity_label='Agenda B',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
invoice_line3 = InvoiceLine.objects.create(
slug='injected',
label='Event A',
event_date=datetime.date(2022, 9, 1),
invoice=invoice3,
quantity=1,
unit_amount=60,
event_slug='injected',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
payment1 = Payment.objects.create(
regie=regie,
amount=35,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
payment_info={
'check_number': '123456',
},
)
payment1.set_number()
payment1.save()
InvoiceLinePayment.objects.create(
payment=payment1,
line=invoice_line1,
amount=35,
)
payment2 = 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_info={
'check_number': '123456',
'check_issuer': 'Foo',
'check_bank': 'Bar',
'payment_reference': 'Ref',
},
)
payment2.set_number()
payment2.save()
InvoiceLinePayment.objects.create(
payment=payment2,
line=invoice_line1,
amount=5,
)
InvoiceLinePayment.objects.create(
payment=payment2,
line=invoice_line2,
amount=50,
)
docket = PaymentDocket.objects.create(
regie=regie, date_end=now().date() + datetime.timedelta(days=1), draft=False
)
docket.set_number()
docket.save()
payment3 = Payment.objects.create(
regie=regie,
amount=2,
payment_type=PaymentType.objects.get(regie=regie, slug='creditcard'),
payer_external_id='payer:3',
payer_first_name='First3',
payer_last_name='Name3',
payer_address='43 rue des kangourous\n99999 Kangourou Ville',
docket=docket,
)
payment3.set_number()
payment3.save()
InvoiceLinePayment.objects.create(
payment=payment3,
line=invoice_line3,
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
invoice2.refresh_from_db()
assert invoice2.remaining_amount == 0
assert invoice2.paid_amount == 50
invoice3.refresh_from_db()
assert invoice3.remaining_amount == 58
assert invoice3.paid_amount == 2
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/payments/' % regie.pk)
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % payment1.pk
).text() == 'Payment R%02d-%s-0000001 dated %s from First1 Name1, amount 35.00€ (Cash) - download' % (
regie.pk,
payment1.created_at.strftime('%y-%m'),
payment1.created_at.strftime('%d/%m/%Y'),
)
assert [
PyQuery(tr).text() for tr in resp.pyquery('tr[data-related-invoicing-element-id="%s"]' % payment1.pk)
] == [
'Invoice\nAmount charged\nAmount assigned',
'F%02d-%s-0000001\n40.00€\n35.00€'
% (
regie.pk,
invoice1.created_at.strftime('%y-%m'),
),
'Number: 123456',
'Cancel payment',
]
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % payment2.pk
).text() == 'Payment R%02d-%s-0000002 dated %s from First1 Name1, amount 55.00€ (Check) - download' % (
regie.pk,
payment2.created_at.strftime('%y-%m'),
payment2.created_at.strftime('%d/%m/%Y'),
)
assert [
PyQuery(tr).text() for tr in resp.pyquery('tr[data-related-invoicing-element-id="%s"]' % payment2.pk)
] == [
'Invoice\nAmount charged\nAmount assigned',
'F%02d-%s-0000001\n40.00€\n5.00€'
% (
regie.pk,
invoice1.created_at.strftime('%y-%m'),
),
'F%02d-%s-0000002\n50.00€\n50.00€'
% (
regie.pk,
invoice2.created_at.strftime('%y-%m'),
),
'Issuer: Foo',
'Bank/Organism: Bar',
'Number: 123456',
'Reference: Ref',
'Cancel payment',
]
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % payment3.pk
).text() == 'Payment R%02d-%s-0000003 dated %s from First3 Name3, amount 2.00€ (Credit card) - download' % (
regie.pk,
payment3.created_at.strftime('%y-%m'),
payment3.created_at.strftime('%d/%m/%Y'),
)
assert [
PyQuery(tr).text() for tr in resp.pyquery('tr[data-related-invoicing-element-id="%s"]' % payment3.pk)
] == [
'Invoice\nAmount charged\nAmount assigned',
'F%02d-%s-0000003\n60.00€\n2.00€'
% (
regie.pk,
invoice3.created_at.strftime('%y-%m'),
),
'Docket: B%02d-%s-0000001' % (regie.pk, docket.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 + 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,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,
invoice3.created_at.strftime('%y-%m'),
payment3.created_at.strftime('%Y-%m-%d'),
regie.pk,
payment2.created_at.strftime('%y-%m'),
regie.pk,
invoice1.created_at.strftime('%y-%m'),
payment2.created_at.strftime('%Y-%m-%d'),
regie.pk,
payment2.created_at.strftime('%y-%m'),
regie.pk,
invoice2.created_at.strftime('%y-%m'),
payment2.created_at.strftime('%Y-%m-%d'),
regie.pk,
payment1.created_at.strftime('%y-%m'),
regie.pk,
invoice1.created_at.strftime('%y-%m'),
payment1.created_at.strftime('%Y-%m-%d'),
)
# test filters
today = datetime.date.today()
params = [
({'number': payment1.formatted_number}, 1, 1),
({'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')}, 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'}, 2, 2),
({'payer_first_name': 'first'}, 4, 5),
({'payer_first_name': 'first1'}, 2, 3),
({'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}, 2, 3),
(
{
'amount_min': '2',
'amount_min_lookup': 'gt',
},
2,
3,
),
(
{
'amount_min': '2',
'amount_min_lookup': 'gte',
},
4,
5,
),
(
{
'amount_max': '55',
'amount_max_lookup': 'lt',
},
3,
3,
),
(
{
'amount_max': '55',
'amount_max_lookup': 'lte',
},
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(
'/manage/invoicing/regie/%s/payments/' % regie.pk,
params=param,
)
assert len(resp.pyquery('tr.payment')) == result
param['csv'] = True
resp = app.get(
'/manage/invoicing/regie/%s/payments/' % regie.pk,
params=param,
)
assert len([a for a in resp.text.split('\r\n') if a]) == 1 + csv_result
def test_regie_payment_pdf(app, admin_user):
regie = Regie.objects.create(
label='Foo',
cashier_name='Le régisseur principal',
city_name='Kangourou Ville',
invoice_main_colour='#9141ac',
)
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,
)
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/payment/%s/pdf/?html' % (regie.pk, payment.pk))
assert 'color: #9141ac;' in resp
assert resp.pyquery('#document-label').text() == 'Payment receipt'
assert resp.pyquery('.address-to-container').text() == (
'Invoiced account:\nFirst1 Name1 (1)\nInvoicing address:\n41 rue des kangourous\n99999 Kangourou Ville'
)
assert resp.pyquery('p#informations').text() == (
'Hereby certifies that I have received a payment of type CHECK in the amount of 55.00€ (number R%02d-%s-0000001), for account 1 First1 Name1:'
) % (
regie.pk,
payment.created_at.strftime('%y-%m'),
)
assert [PyQuery(tr).text() for tr in resp.pyquery.find('thead tr')] == [
'Invoice number\nInvoice object\nAmount charged\nAmount assigned'
]
assert [PyQuery(tr).text() for tr in resp.pyquery.find('tbody tr')] == [
'F%02d-%s-0000001\n40.00€\n5.00€'
% (
regie.pk,
invoice1.created_at.strftime('%y-%m'),
),
'F%02d-%s-0000002\n50.00€\n50.00€'
% (
regie.pk,
invoice2.created_at.strftime('%y-%m'),
),
]
assert resp.pyquery(
'#regie-signature'
).text() == 'Le régisseur principal\nKangourou Ville, on %s' % payment.created_at.strftime('%d/%m/%Y')
resp = app.get('/manage/invoicing/regie/%s//payment/%s/pdf/?html' % (0, payment.pk), status=404)
resp = app.get('/manage/invoicing/regie/%s//payment/%s/pdf/?html' % (regie.pk, 0), status=404)
other_regie = Regie.objects.create(label='Foo')
resp = app.get(
'/manage/invoicing/regie/%s/payment/%s/pdf/?html' % (other_regie.pk, payment.pk), status=404
)
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)

View File

@ -1,6 +1,7 @@
import copy
import pytest
from django.contrib.auth.models import Group
from lingo.invoicing.models import Payer, PaymentType, Regie
from lingo.invoicing.utils import export_site, import_site
@ -25,19 +26,54 @@ def test_import_export_regies(app):
payload = export_site()
assert len(payload['regies']) == 0
regie = Regie.objects.create(label='Foo bar')
group = Group.objects.create(name='role-foo')
regie = Regie.objects.create(
label='Foo bar',
description='blah',
cashier_role=group,
counter_name='{yyyy}',
invoice_number_format='Fblah{regie_id:02d}-{yy}-{mm}-{number:07d}',
payment_number_format='Rblah{regie_id:02d}-{yy}-{mm}-{number:07d}',
docket_number_format='Bblah{regie_id:02d}-{yy}-{mm}-{number:07d}',
credit_number_format='Ablah{regie_id:02d}-{yy}-{mm}-{number:07d}',
refund_number_format='Vblah{regie_id:02d}-{yy}-{mm}-{number:07d}',
invoice_model='full',
invoice_custom_text='foo bar',
invoice_main_colour='#DF5A14',
cashier_name='Foo',
city_name='Bar',
)
payload = export_site()
assert len(payload['regies']) == 1
regie.delete()
group.delete()
assert not Regie.objects.exists()
Group.objects.create(name='role')
with pytest.raises(LingoImportError) as excinfo:
import_site(copy.deepcopy(payload))
assert str(excinfo.value) == 'Missing role: role-foo'
group = Group.objects.create(name='role-foo')
import_site(copy.deepcopy(payload))
assert Regie.objects.count() == 1
regie = Regie.objects.first()
assert regie.label == 'Foo bar'
assert regie.slug == 'foo-bar'
assert regie.cashier_role == group
assert regie.counter_name == '{yyyy}'
assert regie.invoice_number_format == 'Fblah{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.payment_number_format == 'Rblah{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.docket_number_format == 'Bblah{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.credit_number_format == 'Ablah{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.refund_number_format == 'Vblah{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.invoice_model == 'full'
assert regie.invoice_custom_text == 'foo bar'
assert regie.invoice_main_colour == '#DF5A14'
assert regie.cashier_name == 'Foo'
assert regie.city_name == 'Bar'
# update
update_payload = copy.deepcopy(payload)

View File

@ -867,13 +867,18 @@ def test_counter():
counter1_bis = Counter.objects.get(regie=regie1, name='foo', kind='payment')
assert counter1_bis.value == 1
assert Counter.get_count(regie=regie1, name='foo', kind='credit') == 1
assert Counter.get_count(regie=regie1, name='foo', kind='docket') == 1
assert Counter.objects.count() == 3
counter1_bis = Counter.objects.get(regie=regie1, name='foo', kind='docket')
assert counter1_bis.value == 1
assert Counter.get_count(regie=regie1, name='foo', kind='credit') == 1
assert Counter.objects.count() == 4
counter1_ter = Counter.objects.get(regie=regie1, name='foo', kind='credit')
assert counter1_ter.value == 1
assert Counter.get_count(regie=regie1, name='foo', kind='refund') == 1
assert Counter.objects.count() == 4
assert Counter.objects.count() == 5
counter1_ter = Counter.objects.get(regie=regie1, name='foo', kind='refund')
assert counter1_ter.value == 1
@ -890,14 +895,14 @@ def test_counter():
assert counter1.value == 3
assert Counter.get_count(regie=regie2, name='foo', kind='invoice') == 1
assert Counter.objects.count() == 5
assert Counter.objects.count() == 6
counter1.refresh_from_db()
assert counter1.value == 3
counter2 = Counter.objects.get(regie=regie2, name='foo', kind='invoice')
assert counter2.value == 1
assert Counter.get_count(regie=regie2, name='bar', kind='invoice') == 1
assert Counter.objects.count() == 6
assert Counter.objects.count() == 7
counter1.refresh_from_db()
assert counter1.value == 3
counter2.refresh_from_db()
@ -929,6 +934,7 @@ def test_regie_format_number():
regie = Regie.objects.create()
assert regie.invoice_number_format == 'F{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.payment_number_format == 'R{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.docket_number_format == 'B{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.credit_number_format == 'A{regie_id:02d}-{yy}-{mm}-{number:07d}'
assert regie.refund_number_format == 'V{regie_id:02d}-{yy}-{mm}-{number:07d}'
@ -954,6 +960,17 @@ def test_regie_format_number():
assert regie.format_number(datetime.date(2023, 2, 15), 42, 'payment') == 'Rfoobar-2023-00000042'
assert regie.format_number(datetime.date(2024, 12, 15), 42000000, 'payment') == 'Rfoobar-2024-42000000'
assert regie.format_number(datetime.date(2023, 2, 15), 42, 'docket') == 'B%02d-23-02-0000042' % regie.pk
assert (
regie.format_number(datetime.date(2024, 12, 15), 42000000, 'docket')
== 'B%02d-24-12-42000000' % regie.pk
)
regie.docket_number_format = 'Bfoobar-{yyyy}-{number:08d}'
regie.save()
assert regie.format_number(datetime.date(2023, 2, 15), 42, 'docket') == 'Bfoobar-2023-00000042'
assert regie.format_number(datetime.date(2024, 12, 15), 42000000, 'docket') == 'Bfoobar-2024-42000000'
assert regie.format_number(datetime.date(2023, 2, 15), 42, 'credit') == 'A%02d-23-02-0000042' % regie.pk
assert (
regie.format_number(datetime.date(2024, 12, 15), 42000000, 'credit')