invoicing: list non invoiced lines (#73741) #22

Merged
lguerin merged 1 commits from wip/73741-non-invoiced-lines into main 2023-02-04 15:43:04 +01:00
7 changed files with 309 additions and 4 deletions

View File

@ -9,6 +9,7 @@
{% block appbar %} {% block appbar %}
<h2>{% trans "Campaigns" %}</h2> <h2>{% trans "Campaigns" %}</h2>
<span class="actions"> <span class="actions">
<a href="{% url 'lingo-manager-invoicing-non-invoiced-line-list' %}">{% trans 'Non invoiced lines' %}</a>
<a rel="popup" href="{% url 'lingo-manager-invoicing-campaign-add' %}">{% trans 'New campaign' %}</a> <a rel="popup" href="{% url 'lingo-manager-invoicing-campaign-add' %}">{% trans 'New campaign' %}</a>
</span> </span>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,90 @@
{% extends "lingo/invoicing/manager_campaign_list.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-non-invoiced-line-list' %}">{% trans "Non invoiced lines" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Non invoiced lines" %}</h2>
{% endblock %}
{% block content %}
<div>
<table class="main lines">
<thead>
<tr>
<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 object_list %}
<tr data-line-id="{% if line.status == 'injected' %}injected{% else %}line{% endif %}-{{ line.pk }}">
<td>
{{ line.event_date|date:"d/m/Y" }} - {{ line.label }}
<br />
{% if line.status == 'error'%}
<a href="{% url 'lingo-manager-invoicing-pool-journal' pk=line.campaign_id pool_pk=line.pool_id %}?pk={{ line.pk }}">({{ line.slug }})</a>
{% else %}
({{ line.slug }})
{% endif %}
</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 }}">{% spaceless %}
{% if line.status == 'error'%}
{% trans "Error" %}
{% else %}
{% trans "Injected" %}
{% endif %}
{% endspaceless %}</span>
{% if line.status == 'error'%}
({{ line.error_display }})
{% endif %}
</td>
<td>{% if line.status != 'injected' %}<a class="details-toggle" href="#">{% trans "see details" %}</a>{% endif %}</td>
</tr>
{% if line.status != 'injected' %}
<tr data-details-for-line-id="line-{{ 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>
{% endif %}
{% endfor %}
</tbody>
</table>
{% include "gadjo/pagination.html" %}
</div>
<script>
$(function() {
$(document).on('click', 'a.details-toggle', function(event) {
var line_id = $(this).parents('tr').data('line-id');
var $details = $('tr[data-details-for-line-id=' + line_id + ']');
if ($details.is(':visible')) {
$(this).text('{% trans "see details" %}');
$details.hide();
} else {
$(this).text('{% trans "hide details" %}');
$details.show();
}
event.preventDefault();
});
});
</script>
{% endblock %}

View File

@ -39,7 +39,7 @@
}); });
</script> </script>
</form> </form>
<table class="main pools"> <table class="main lines">
<thead> <thead>
<tr> <tr>
<th>{% trans "PK" %}</th> <th>{% trans "PK" %}</th>

View File

@ -94,4 +94,9 @@ urlpatterns = [
views.line_set_error_status, views.line_set_error_status,
name='lingo-manager-invoicing-line-set-error-status', name='lingo-manager-invoicing-line-set-error-status',
), ),
path(
'campaigns/non-invoiced-lines/',
views.non_invoiced_line_list,
name='lingo-manager-invoicing-non-invoiced-line-list',
),
] ]

View File

@ -19,8 +19,9 @@ import datetime
import json import json
from django.contrib import messages from django.contrib import messages
from django.contrib.postgres.fields import JSONField
from django.db import transaction from django.db import transaction
from django.db.models import Count, IntegerField, OuterRef, Subquery, Value from django.db.models import CharField, Count, IntegerField, OuterRef, Subquery, Value
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
@ -43,6 +44,7 @@ from lingo.invoicing.models import (
Campaign, Campaign,
DraftInvoice, DraftInvoice,
DraftInvoiceLine, DraftInvoiceLine,
InjectedLine,
InvoiceLine, InvoiceLine,
Pool, Pool,
Regie, Regie,
@ -495,3 +497,54 @@ class LineSetErrorStatusView(DetailView):
line_set_error_status = LineSetErrorStatusView.as_view() line_set_error_status = LineSetErrorStatusView.as_view()
class NonInvoicedLineListView(ListView):
template_name = 'lingo/invoicing/manager_non_invoiced_line_list.html'
paginate_by = 100
def get_queryset(self):
fields = [
'pk',
'event_date',
'slug',
'label',
'quantity',
'unit_amount',
'total_amount',
'user_external_id',
'payer_external_id',
'user_name',
'event',
'pricing_data',
'status',
'pool_id',
]
qs1 = InvoiceLine.objects.filter(status='error', error_status='').values(*fields)
qs2 = (
InjectedLine.objects.filter(invoiceline__isnull=True)
.annotate(
user_name=Value('', output_field=CharField()),
event=Value({}, output_field=JSONField()),
pricing_data=Value({}, output_field=JSONField()),
status=Value('injected', output_field=CharField()),
pool_id=Value(0, output_field=IntegerField()),
)
.values(*fields)
)
qs = qs1.union(qs2).order_by('event_date', 'user_external_id', 'label', 'pk')
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pools = Pool.objects.filter(draft=False).in_bulk()
for line in context['object_list']:
if line['status'] == 'error':
line['error_display'] = InvoiceLine(
status=line['status'], pricing_data=line['pricing_data']
).get_error_display()
line['campaign_id'] = pools[line['pool_id']].campaign_id
return context
non_invoiced_line_list = NonInvoicedLineListView.as_view()

View File

@ -106,7 +106,7 @@ div.test-tool-result .infonotice h3 {
#panel-pools span.tag, #panel-pools span.tag,
h2#pool-title span.tag, h2#pool-title span.tag,
table.pools span.tag { table.lines span.tag {
box-sizing: border-box; box-sizing: border-box;
border: none; border: none;
border-radius: 1ex; border-radius: 1ex;

View File

@ -3,7 +3,16 @@ from unittest import mock
import pytest import pytest
from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Invoice, InvoiceLine, Pool, Regie from lingo.invoicing.models import (
Campaign,
DraftInvoice,
DraftInvoiceLine,
InjectedLine,
Invoice,
InvoiceLine,
Pool,
Regie,
)
from tests.utils import login from tests.utils import login
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -1277,3 +1286,150 @@ def test_set_error_status_line(app, admin_user):
app.get( app.get(
'/manage/invoicing/ajax/campaign/%s/pool/%s/line/%s/reset/' % (campaign.pk, pool.pk, 0), status=404 '/manage/invoicing/ajax/campaign/%s/pool/%s/line/%s/reset/' % (campaign.pk, pool.pk, 0), status=404
) )
def test_non_invoiced_line_list(app, admin_user):
regie = Regie.objects.create(label='Regie')
campaign = Campaign.objects.create(
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
date_issue=datetime.date(2022, 10, 31),
)
pool = Pool.objects.create(
campaign=campaign,
draft=True,
)
other_campaign = Campaign.objects.create(
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
date_issue=datetime.date(2022, 10, 31),
)
other_pool = Pool.objects.create(
campaign=other_campaign,
draft=False,
)
# not invoiced
InjectedLine.objects.create(
event_date=datetime.date(2022, 9, 1),
slug='event-2022-09-01',
label='Event 2022-09-01',
quantity=2,
unit_amount=1.5,
total_amount=3,
user_external_id='user:1',
payer_external_id='payer:1',
regie=regie,
)
# not invoiced, but linked in a DraftInvoiceLine
injected_line2 = InjectedLine.objects.create(
event_date=datetime.date(2022, 9, 2),
slug='event-2022-09-02',
label='Event 2022-09-02',
quantity=2,
unit_amount=1.5,
total_amount=3,
user_external_id='user:1',
payer_external_id='payer:1',
regie=regie,
)
DraftInvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 2),
slug='event-2022-09-02',
label='Event 2022-09-02',
quantity=2,
unit_amount=1.5,
total_amount=3,
pool=pool,
from_injected_line=injected_line2,
)
# invoiced, as linked in a non draft pool
injected_line3 = InjectedLine.objects.create(
event_date=datetime.date(2022, 9, 3),
slug='event-2022-09-03',
label='Event 2022-09-03',
quantity=2,
unit_amount=1.5,
total_amount=3,
user_external_id='user:1',
payer_external_id='payer:1',
regie=regie,
)
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 3),
slug='event-2022-09-03',
label='Event 2022-09-03',
quantity=2,
unit_amount=1.5,
total_amount=3,
pool=other_pool,
from_injected_line=injected_line3,
)
# non fixed error
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 4),
slug='event-2022-09-04',
label='Event 2022-09-04',
quantity=0,
unit_amount=0,
total_amount=0,
pool=other_pool,
status='error',
)
# fixed or ignored errors
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 5),
slug='event-2022-09-05',
label='Event 2022-09-05',
quantity=0,
unit_amount=0,
total_amount=0,
pool=other_pool,
status='error',
error_status='fixed',
)
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 6),
slug='event-2022-09-06',
label='Event 2022-09-06',
quantity=0,
unit_amount=0,
total_amount=0,
pool=other_pool,
status='error',
error_status='ignored',
)
# not errors
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 7),
slug='event-2022-09-07',
label='Event 2022-09-07',
quantity=0,
unit_amount=0,
total_amount=0,
pool=other_pool,
status='success',
)
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 8),
slug='event-2022-09-08',
label='Event 2022-09-08',
quantity=0,
unit_amount=0,
total_amount=0,
pool=other_pool,
status='warning',
)
app = login(app)
resp = app.get('/manage/invoicing/campaigns/non-invoiced-lines/')
assert 'event-2022-09-01' in resp
assert 'event-2022-09-02' in resp
assert 'event-2022-09-03' not in resp
assert 'event-2022-09-04' in resp
assert 'event-2022-09-05' not in resp
assert 'event-2022-09-06' not in resp
assert 'event-2022-09-07' not in resp
assert 'event-2022-09-08' not in resp