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 %}
<h2>{% trans "Campaigns" %}</h2>
<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>
</span>
{% 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>
</form>
<table class="main pools">
<table class="main lines">
<thead>
<tr>
<th>{% trans "PK" %}</th>

View File

@ -94,4 +94,9 @@ urlpatterns = [
views.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
from django.contrib import messages
from django.contrib.postgres.fields import JSONField
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.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
@ -43,6 +44,7 @@ from lingo.invoicing.models import (
Campaign,
DraftInvoice,
DraftInvoiceLine,
InjectedLine,
InvoiceLine,
Pool,
Regie,
@ -495,3 +497,54 @@ class LineSetErrorStatusView(DetailView):
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,
h2#pool-title span.tag,
table.pools span.tag {
table.lines span.tag {
box-sizing: border-box;
border: none;
border-radius: 1ex;

View File

@ -3,7 +3,16 @@ from unittest import mock
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
pytestmark = pytest.mark.django_db
@ -1277,3 +1286,150 @@ def test_set_error_status_line(app, admin_user):
app.get(
'/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