invoicing: filter journal (#73688) #19

Merged
lguerin merged 1 commits from wip/73688-journal-filters into main 2023-02-04 15:23:52 +01:00
9 changed files with 265 additions and 71 deletions

View File

@ -15,10 +15,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import django_filters
from django import forms
from django.utils.translation import ugettext_lazy as _
from lingo.invoicing.models import Campaign
from lingo.invoicing.models import Campaign, DraftInvoiceLine, InvoiceLine, Regie
class CampaignForm(forms.ModelForm):
@ -44,3 +45,62 @@ class CampaignForm(forms.ModelForm):
self.add_error(None, _('Another campaign overlapping this period already exists.'))
return cleaned_data
def regie_queryset(request):
return Regie.objects.all()
class AbstractLineFilterSet(django_filters.FilterSet):
regie = django_filters.ModelChoiceFilter(
label=_('Regie'),
queryset=regie_queryset,
field_name='invoice__regie',
)
invoice_number = django_filters.NumberFilter(
label=_('Invoice number'),
field_name='invoice__number',
)
invoice_id = django_filters.NumberFilter(
label=_('Invoice number'),
)
pk = django_filters.NumberFilter(
label=_('PK'),
)
payer_external_id = django_filters.CharFilter(
label=_('Payer (external ID)'),
)
user_external_id = django_filters.CharFilter(
label=_('User (external ID)'),
)
status = django_filters.ChoiceFilter(
label=_('Status'),
choices=[
('success', _('Success')),
('warning', _('Warning')),
('error', _('Error')),
],
widget=forms.RadioSelect,
empty_label=_('all'),
)
def __init__(self, *args, **kwargs):
self.pool = kwargs.pop('pool')
super().__init__(*args, **kwargs)
if self.pool.draft:
del self.filters['invoice_number']
else:
del self.filters['invoice_id']
class DraftInvoiceLineFilterSet(AbstractLineFilterSet):
class Meta:
model = DraftInvoiceLine
fields = []
class InvoiceLineFilterSet(AbstractLineFilterSet):
class Meta:
model = InvoiceLine
fields = []

View File

@ -25,21 +25,24 @@
{% endblock %}
{% block content %}
{% url 'lingo-manager-invoicing-pool-journal' pk=object.pk pool_pk=pool.pk as journal_url %}
<div>
{% for line in lines %}
{% ifchanged line.invoice_id %}
{% if not forloop.first %}</ul>{% endif %}
<h3 data-invoice-id="{{ line.invoice_id }}">
{% if pool.draft %}
{% blocktrans with number=line.invoice_id payer=line.invoice.payer amount=line.invoice.total_amount %}Invoice TMP-{{ number }} addressed to {{ payer }}, amount {{ amount }}€{% endblocktrans %}
{% blocktrans with number=line.invoice_id payer=line.invoice.payer amount=line.invoice.total_amount %}Invoice <a href="{{ journal_url }}?invoice_id={{ number }}">TMP-{{ number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer }}">{{ payer }}</a>, amount {{ amount }}€{% endblocktrans %}
{% else %}
{% blocktrans with number=line.invoice.format_number payer=line.invoice.payer amount=line.invoice.total_amount %}Invoice {{ number }} addressed to {{ payer }}, amount {{ amount }}€{% endblocktrans %}
{% blocktrans with invoice_number=line.invoice.format_number payer=line.invoice.payer amount=line.invoice.total_amount number=line.invoice.number regie_id=line.invoice.regie_id %}Invoice <a href="{{ journal_url }}?invoice_number={{ number }}&regie={{ regie_id }}">{{ invoice_number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer }}">{{ payer }}</a>, amount {{ amount }}€{% endblocktrans %}
{% endif %}
</h3>
<ul class="objects-list" data-invoice-id="{{ line.invoice_id }}">
{% endifchanged %}
<li>
#{{ line.pk }} {{ line.user_name }} - {{ line.event_date|date:"d/m/Y" }} - {{ line.label }} ({{ line.total_amount }})
<a href="{{ journal_url }}?pk={{ line.pk }}">#{{ line.pk }}</a>
<a href="{{ journal_url }}?user_external_id={{ line.user_external_id }}">{{ line.user_name }}</a>
- {{ line.event_date|date:"d/m/Y" }} - {{ line.label }} ({{ line.total_amount }})
</li>
{% if forloop.last %}</ul>{% endif %}
{% endfor %}

View File

@ -21,60 +21,79 @@
{% endblock %}
{% block content %}
{% if pool.status == 'failed' %}
<div class="pk-error">
<p>{% trans "Error while running pool." %}</p>
{% if pool.exception %}<pre>{{ pool.exception }}</pre>{% endif %}
</div>
{% endif %}
<table class="main pools">
<thead>
<tr>
<th>{% trans "PK" %}</th>
<th>{% trans "Invoice PK" %}</th>
<th>{% trans "Event" %}</th>
<th>{% trans "Quantity" %}</th>
<th>{% trans "Unit amount" %}</th>
<th>{% trans "Total amount" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "Payer" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for line in lines %}
<tr data-line-id="{{ line.pk }}">
<td class="line_id">{{ line.pk }}</td>
<td>{{ line.invoice_id|default:'' }}</td>
<td>
{{ line.event_date|date:"d/m/Y" }} - {{ line.label }}
<br />
({{ line.slug }})
</td>
<td>{{ line.quantity }}</td>
<td>{{ line.unit_amount }}</td>
<td>{{ line.total_amount }}</td>
<td>{{ line.user_name }} ({{ line.user_external_id }})</td>
<td>{{ line.payer_external_id }}</td>
<td class="status">
<span class="tag tag-{{ line.status }}">{{ line.get_status_display }}</span>
{% if line.status != 'success' %}({{ line.get_error_display }}){% endif %}
{% if line.from_injected_line_id %}({% trans "Injected" %}){% endif %}
</td>
<td><a class="details-toggle">{% trans "see details" %}</a></td>
<div>
{% if pool.status == 'failed' %}
<div class="pk-error">
<p>{% trans "Error while running pool." %}</p>
{% if pool.exception %}<pre>{{ pool.exception }}</pre>{% endif %}
</div>
{% endif %}
<form class="journal-filters">
{{ filterset.form.as_p }}
<script>
$(function() {
$('form.journal-filters input,select').on('change',
function() {
$(this).parents('form').submit();
});
});
</script>
</form>
<table class="main pools">
<thead>
<tr>
<th>{% trans "PK" %}</th>
<th>{% trans "Invoice number" %}</th>
<th>{% trans "Event" %}</th>
<th>{% trans "Quantity" %}</th>
<th>{% trans "Unit amount" %}</th>
<th>{% trans "Total amount" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "Payer" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
<tr data-details-for-line-id="{{ line.pk }}" style="display: none">
<td colspan="10">
{% trans "Pricing data:" %}
<pre>{{ line.pricing_data|pprint }}</pre>
{% trans "Event:" %}
<pre>{{ line.event|pprint }}</pre>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for line in lines %}
<tr data-line-id="{{ line.pk }}">
<td class="line_id">{{ line.pk }}</td>
<td>
{% if pool.draft and line.invoice %}
TMP-{{ line.invoice_id }}
{% elif line.invoice %}
{{ line.invoice.format_number }}
{% endif %}
</td>
<td>
{{ line.event_date|date:"d/m/Y" }} - {{ line.label }}
<br />
({{ line.slug }})
</td>
<td>{{ line.quantity }}</td>
<td>{{ line.unit_amount }}</td>
<td>{{ line.total_amount }}</td>
<td>{{ line.user_name }} ({{ line.user_external_id }})</td>
<td>{{ line.payer_external_id }}</td>
<td class="status">
<span class="tag tag-{{ line.status }}">{{ line.get_status_display }}</span>
{% if line.status != 'success' %}({{ line.get_error_display }}){% endif %}
{% if line.from_injected_line_id %}({% trans "Injected" %}){% endif %}
</td>
<td><a class="details-toggle">{% trans "see details" %}</a></td>
</tr>
<tr data-details-for-line-id="{{ line.pk }}" style="display: none">
<td colspan="10">
{% trans "Pricing data:" %}
<pre>{{ line.pricing_data|pprint }}</pre>
{% trans "Event:" %}
<pre>{{ line.event|pprint }}</pre>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
$(function() {
$('a.details-toggle').on('click', function() {

View File

@ -38,7 +38,7 @@ from django.views.generic import (
)
from lingo.agendas.models import Agenda
from lingo.invoicing.forms import CampaignForm
from lingo.invoicing.forms import CampaignForm, DraftInvoiceLineFilterSet, InvoiceLineFilterSet
from lingo.invoicing.models import (
Campaign,
DraftInvoice,
@ -341,12 +341,18 @@ class PoolJournalView(DetailView):
kwargs['object'] = self.campaign
kwargs['pool'] = self.object
line_model = InvoiceLine
filter_model = InvoiceLineFilterSet
if self.object.draft:
line_model = DraftInvoiceLine
kwargs['lines'] = line_model.objects.filter(pool=self.object).order_by('pk')
self.object.error_count = len([line for line in kwargs['lines'] if line.status == 'error'])
self.object.warning_count = len([line for line in kwargs['lines'] if line.status == 'warning'])
self.object.success_count = len([line for line in kwargs['lines'] if line.status == 'success'])
filter_model = DraftInvoiceLineFilterSet
all_lines = line_model.objects.filter(pool=self.object).order_by('pk')
self.object.error_count = len([line for line in all_lines if line.status == 'error'])
self.object.warning_count = len([line for line in all_lines if line.status == 'warning'])
self.object.success_count = len([line for line in all_lines if line.status == 'success'])
data = self.request.GET or None
line_filterset = filter_model(data=data, queryset=all_lines, pool=self.object)
kwargs['lines'] = line_filterset.qs if data and [v for v in data.values() if v] else all_lines
kwargs['filterset'] = line_filterset
return super().get_context_data(**kwargs)

View File

@ -123,3 +123,8 @@ table.pools span.tag {
background: #f64474;
}
}
form.journal-filters li {
display: inline;
margin-right: 10px;
}

View File

@ -55,6 +55,7 @@ INSTALLED_APPS = (
'eopayment',
'gadjo',
'rest_framework',
'django_filters',
'lingo.agendas',
'lingo.api',
'lingo.invoicing',

View File

@ -165,6 +165,7 @@ setup(
'requests',
'eopayment>=1.60',
'djangorestframework>=3.3, <3.13',
'django-filter',
],
zip_safe=False,
cmdclass={

View File

@ -742,7 +742,11 @@ def test_journal_pool(app, admin_user):
assert 'tag-error' not in resp
def test_journal_pool_lines(app, admin_user):
@pytest.mark.parametrize('draft', [True, False])
def test_journal_pool_lines(app, admin_user, draft):
invoice_model = DraftInvoice if draft else Invoice
line_model = DraftInvoiceLine if draft else InvoiceLine
campaign = Campaign.objects.create(
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
@ -750,16 +754,27 @@ def test_journal_pool_lines(app, admin_user):
)
pool = Pool.objects.create(
campaign=campaign,
draft=True,
draft=draft,
status='completed',
)
regie = Regie.objects.create(label='Foo')
invoice = DraftInvoice.objects.create(date_issue=datetime.date.today(), regie=regie, pool=pool)
regie1 = Regie.objects.create(label='Foo1')
regie2 = Regie.objects.create(label='Foo1')
invoice1 = invoice_model.objects.create(
date_issue=datetime.date.today(), regie=regie1, pool=pool, payer='payer:1'
)
invoice2 = invoice_model.objects.create(
date_issue=datetime.date.today(), regie=regie2, pool=pool, payer='payer:2'
)
if not draft:
invoice1.number = 42
invoice1.save()
invoice2.number = 35
invoice2.save()
lines = [
DraftInvoiceLine.objects.create(
line_model.objects.create(
event_date=datetime.date(2022, 9, 1),
invoice=invoice,
invoice=invoice1,
quantity=1,
unit_amount=1,
total_amount=1,
@ -767,7 +782,9 @@ def test_journal_pool_lines(app, admin_user):
pool=pool,
pricing_data={'foo': 'bar'},
event={'event': 'foobar'},
)
user_external_id='user:1',
payer_external_id='payer:1',
),
]
errors = [
('AgendaPricingNotFound', {}),
@ -795,9 +812,8 @@ def test_journal_pool_lines(app, admin_user):
]
for error, error_details in errors:
lines.append(
DraftInvoiceLine.objects.create(
line_model.objects.create(
event_date=datetime.date(2022, 9, 1),
invoice=invoice,
quantity=1,
unit_amount=1,
total_amount=1,
@ -808,9 +824,27 @@ def test_journal_pool_lines(app, admin_user):
'error_details': error_details,
},
event={'event': 'foobar'},
user_external_id='user:1',
payer_external_id='payer:2',
)
)
lines.append(
line_model.objects.create(
event_date=datetime.date(2022, 9, 1),
invoice=invoice2,
quantity=1,
unit_amount=1,
total_amount=1,
status='success',
pool=pool,
pricing_data={'foo': 'bar'},
event={'event': 'foobar2'},
user_external_id='user:2',
payer_external_id='payer:2',
),
)
def format_status(value):
return (' '.join([v.strip() for v in value.split('\n')])).strip()
@ -917,6 +951,70 @@ def test_journal_pool_lines(app, admin_user):
"{'error': 'PricingBookingCheckTypeError', 'error_details': {'check_type': 'foo-reason', "
"'check_type_group': 'foo-bar', 'reason': 'wrong-kind'}} {'event': 'foobar'}"
)
assert format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[13].pk).text()) == 'Success'
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[13].pk).text().strip()
== "{'foo': 'bar'} {'event': 'foobar2'}"
)
# test filters
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'regie': regie1.pk},
)
assert len(resp.pyquery('tr td.status')) == 1
if draft:
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'invoice_id': invoice1.pk},
)
assert len(resp.pyquery('tr td.status')) == 1
else:
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'invoice_number': invoice1.number},
)
assert len(resp.pyquery('tr td.status')) == 1
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'pk': lines[0].pk},
)
assert len(resp.pyquery('tr td.status')) == 1
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'payer_external_id': 'payer:1'},
)
assert len(resp.pyquery('tr td.status')) == 1
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'payer_external_id': 'payer:2'},
)
assert len(resp.pyquery('tr td.status')) == 13
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'user_external_id': 'user:1'},
)
assert len(resp.pyquery('tr td.status')) == 13
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'user_external_id': 'user:2'},
)
assert len(resp.pyquery('tr td.status')) == 1
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'status': 'success'},
)
assert len(resp.pyquery('tr td.status')) == 2
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'status': 'warning'},
)
assert len(resp.pyquery('tr td.status')) == 1
resp = app.get(
'/manage/invoicing/campaign/%s/pool/%s/journal/' % (campaign.pk, pool.pk),
params={'status': 'error'},
)
assert len(resp.pyquery('tr td.status')) == 11
def test_delete_pool(app, admin_user):

View File

@ -34,6 +34,7 @@ deps =
django32: psycopg2-binary
django22: psycopg2-binary<2.9
django-mellon>=1.13
django-filter>=2.4,<2.5
Outdated
Review

plutôt django-filter>=2.4,<2.5 quelle que soit la version de Django (pour suivre Debian 11)

plutôt django-filter>=2.4,<2.5 quelle que soit la version de Django (pour suivre Debian 11)
git+https://git.entrouvert.org/publik-django-templatetags.git
pre-commit
commands =