invoicing: list non invoiced lines (#73741) #22
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
|
@ -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>
|
||||||
|
|
|
@ -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',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue