invoicing: add description field on line models (#84855)

This commit is contained in:
Lauréline Guérin 2023-12-19 16:30:22 +01:00 committed by Lauréline Guérin
parent 796c67702b
commit 25a5013244
19 changed files with 100 additions and 85 deletions

View File

@ -266,6 +266,7 @@ class DraftInvoiceSerializer(serializers.ModelSerializer):
class DraftInvoiceLineSerializer(serializers.ModelSerializer):
activity_label = serializers.CharField(required=False, max_length=250)
description = serializers.CharField(required=False, max_length=500)
class Meta:
model = DraftInvoiceLine
@ -276,6 +277,7 @@ class DraftInvoiceLineSerializer(serializers.ModelSerializer):
'quantity',
'unit_amount',
'activity_label',
'description',
'user_external_id',
'user_first_name',
'user_last_name',

View File

@ -58,4 +58,16 @@ class Migration(migrations.Migration):
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='draftinvoiceline',
name='description',
field=models.CharField(default='', max_length=500),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceline',
name='description',
field=models.CharField(default='', max_length=500),
preserve_default=False,
),
]

View File

@ -1,31 +1,42 @@
from django.conf import settings
from django.db import migrations
from django.utils import dateparse, formats, translation
def forward(apps, schema_editor):
DraftInvoiceLine = apps.get_model('invoicing', 'DraftInvoiceLine')
InvoiceLine = apps.get_model('invoicing', 'InvoiceLine')
Agenda = apps.get_model('agendas', 'Agenda')
agendas_by_slug = {a.slug: a for a in Agenda.objects.all()}
with translation.override(settings.LANGUAGE_CODE):
DraftInvoiceLine = apps.get_model('invoicing', 'DraftInvoiceLine')
InvoiceLine = apps.get_model('invoicing', 'InvoiceLine')
Agenda = apps.get_model('agendas', 'Agenda')
agendas_by_slug = {a.slug: a for a in Agenda.objects.all()}
for line_model in [DraftInvoiceLine, InvoiceLine]:
for line in line_model.objects.all():
# init event_slug
if line.details.get('agenda'):
line.event_slug = '%s@%s' % (line.details['agenda'], line.details['primary_event'])
else:
line.event_slug = line.slug
for line_model in [DraftInvoiceLine, InvoiceLine]:
for line in line_model.objects.all():
# init event_slug
if line.details.get('agenda'):
line.event_slug = '%s@%s' % (line.details['agenda'], line.details['primary_event'])
else:
line.event_slug = line.slug
# init agenda_slug
if '@' in line.event_slug:
line.agenda_slug = line.event_slug.split('@')[0]
# init agenda_slug
if '@' in line.event_slug:
line.agenda_slug = line.event_slug.split('@')[0]
# init activity_label
agenda = agendas_by_slug.get(line.agenda_slug)
if agenda:
line.activity_label = agenda.label
agenda = agendas_by_slug.get(line.agenda_slug)
if agenda:
# init activity_label
line.activity_label = agenda.label
# save line
line.save()
# init description
if line.details.get('dates') and not agenda.partial_bookings:
line.description = ', '.join(
formats.date_format(dateparse.parse_date(d), 'Dd') for d in line.details['dates']
)
# and fix partial_bookings
line.details['partial_bookings'] = agenda.partial_bookings
# save line
line.save()
class Migration(migrations.Migration):

View File

@ -851,11 +851,8 @@ class AbstractInvoice(AbstractInvoiceObject):
def get_grouped_and_ordered_lines(self):
lines = []
agendas_by_slug = {a.slug: a for a in Agenda.objects.all()}
possible_status = ['presence', 'absence', 'cancelled', '']
for line in self.lines.all():
# search for agenda
line.agenda = agendas_by_slug.get(line.agenda_slug)
# build event date/datetime, and check status
event_date = line.event_date.strftime('Y-m-d')
event_status = 'z' * 5000
@ -904,11 +901,7 @@ class AbstractInvoice(AbstractInvoiceObject):
'appearance_settings': AppearanceSettings.singleton(),
}
if self.pool and self.pool.campaign.invoice_model == 'full':
context['lines_for_details'] = [
li
for li in lines
if li.details.get('dates') and (not li.agenda or not li.agenda.partial_bookings)
]
context['lines_for_details'] = [li for li in lines if li.description]
return template.render(context)
def payments_html(self):
@ -1122,6 +1115,7 @@ class AbstractInvoiceLine(AbstractInvoiceLineObject):
event_slug = models.CharField(max_length=250)
agenda_slug = models.CharField(max_length=250)
activity_label = models.CharField(max_length=250)
description = models.CharField(max_length=500)
pool = models.ForeignKey(Pool, on_delete=models.PROTECT, null=True)

View File

@ -1,5 +1,5 @@
{% extends "lingo/invoicing/print_document_base.html" %}
{% load i18n lingo %}
{% load i18n %}
{% block document-label %}{{ credit.label }}{% endblock %}

View File

@ -1,5 +1,5 @@
{% extends "lingo/invoicing/print_document_base.html" %}
{% load i18n lingo %}
{% load i18n %}
{% block document-label %}{{ invoice.label }}{% endblock %}
@ -55,19 +55,17 @@
{% if line.details.check_type_label %}
{{ line.details.check_type_label }}
{% elif line.details.status == 'absence' %}
{% if invoice.pool.campaign.invoice_model == 'middle' or line.agenda.partial_bookings %}
{% if invoice.pool.campaign.invoice_model == 'middle' or line.details.partial_bookings %}
{% trans "Absence" %}
{% endif %}
{% endif %}
</td>
{% if invoice.pool.campaign.invoice_model == 'middle' %}
<td class="details">
{% if line.details.dates and not line.agenda.partial_bookings %}
{% for d in line.details.dates %}{{ d|parse_date|date:"Dd" }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% endif %}
{{ line.description }}
</td>
{% endif %}
{% if line.details.check_type_label or line.details.status != 'absence' or line.agenda.partial_bookings %}
{% if line.details.check_type_label or line.details.status != 'absence' or line.details.partial_bookings %}
<td class="amount unit-amount">{% blocktrans with amount=line.unit_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="quantity">{{ line.quantity|floatformat }}</td>
<td class="amount total-amount">{% blocktrans with amount=line.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
@ -147,7 +145,7 @@
{% endif %}
</td>
<td class="details">
{% for d in line.details.dates %}{{ d|parse_date|date:"Dd" }}{% if not forloop.last %}, {% endif %}{% endfor %}
{{ line.description }}
</td>
</tr>
{% endfor %}

View File

@ -1,4 +1,4 @@
{% load i18n lingo %}
{% load i18n %}
{% for line in object_list %}
{% ifchanged line.user_external_id %}
<tr class="line" data-related-invoicing-element-id="{{ credit.pk }}">

View File

@ -1,4 +1,4 @@
{% load i18n lingo %}
{% load i18n %}
<tr class="line" data-related-invoicing-element-id="{{ invoice.pk }}">
<td colspan="2"></td>
<td class="invoice-details" colspan="4">
@ -81,9 +81,7 @@
{% elif line.details.status == 'absence' %}
{% trans "Absence" %}
{% endif %}
{% if line.details.dates and not line.agenda.partial_bookings %}
{% for d in line.details.dates %}{{ d|parse_date|date:"Dd" }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% endif %}
{{ line.description }}
</small>
</td>
<td class="amount">

View File

@ -1,5 +1,5 @@
{% extends "lingo/invoicing/print_document_base.html" %}
{% load i18n lingo %}
{% load i18n %}
{% block document-label %}{% trans "Payment receipt" %}{% endblock %}

View File

@ -1,5 +1,5 @@
{% extends "lingo/invoicing/print_document_base.html" %}
{% load i18n lingo %}
{% load i18n %}
{% block document-label %}{% trans "Payments certificate" %}{% endblock %}

View File

@ -1,4 +1,4 @@
{% load static i18n lingo %}<!DOCTYPE html><html>
{% load static i18n %}<!DOCTYPE html><html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{% block title %}{{ object.formatted_number }}{% endblock %}</title>

View File

@ -19,6 +19,7 @@ import datetime
from django.db import transaction
from django.test.client import RequestFactory
from django.utils import formats
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
@ -396,6 +397,10 @@ def generate_invoices_from_lines(all_lines, pool):
if key[7] == 'minutes':
quantity = quantity / 60 # convert in hours
agenda = agendas_by_slug.get(key[1])
dates = sorted(li.event_date for li in journal_lines)
description = ''
if agenda and dates and not agenda.partial_bookings:
description = ', '.join(formats.date_format(d, 'Dd') for d in dates)
invoice_line = DraftInvoiceLine.objects.create(
invoice=invoice,
event_date=pool.campaign.date_start,
@ -410,12 +415,14 @@ def generate_invoices_from_lines(all_lines, pool):
'check_type': key[4],
'check_type_group': key[5],
'check_type_label': check_type.label if check_type else key[4],
'dates': sorted(li.event_date for li in journal_lines),
'dates': dates,
'event_time': event_time,
'partial_bookings': agenda.partial_bookings if agenda else False,
},
event_slug='%s@%s' % (key[1], key[2]),
agenda_slug=key[1],
activity_label=agenda.label if agenda else '',
description=description,
user_external_id=first_line.user_external_id,
user_first_name=first_line.user_first_name,
user_last_name=first_line.user_last_name,

View File

@ -1,28 +0,0 @@
# lingo - payment and billing system
# Copyright (C) 2023 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django import template
from django.utils import dateparse
register = template.Library()
@register.filter
def parse_date(date_string):
try:
return dateparse.parse_date(date_string)
except (ValueError, TypeError):
return None

View File

@ -2041,6 +2041,7 @@ def test_add_draft_invoice_line(app, user):
assert line.event_slug == 'bar-foo'
assert line.agenda_slug == ''
assert line.activity_label == ''
assert line.description == ''
assert line.pool is None
invoice.refresh_from_db()
assert invoice.total_amount == 42
@ -2051,6 +2052,7 @@ def test_add_draft_invoice_line(app, user):
'quantity': '2',
'slug': 'agenda@bar-foo',
'activity_label': 'Activity Label !',
'description': 'A description !',
'unit_amount': '21',
'user_external_id': 'user:1',
'user_first_name': 'First1',
@ -2080,6 +2082,7 @@ def test_add_draft_invoice_line(app, user):
assert line.event_slug == 'agenda@bar-foo'
assert line.agenda_slug == 'agenda'
assert line.activity_label == 'Activity Label !'
assert line.description == 'A description !'
assert line.pool is None
invoice.refresh_from_db()
assert invoice.total_amount == 84

View File

@ -1442,6 +1442,7 @@ def test_detail_pool_invoices(app, admin_user, draft):
event_slug='agenda-a@event-a',
agenda_slug='agenda-a',
activity_label='Agenda A',
description='Thu01, Fri02, Sat03',
)
invoice_line12 = line_model.objects.create(
slug='event-b-foo-bar',
@ -1466,6 +1467,7 @@ def test_detail_pool_invoices(app, admin_user, draft):
event_slug='agenda-b@event-b',
agenda_slug='agenda-b',
activity_label='Agenda B',
description='Thu01, Fri02, Sat03',
)
invoice_line13 = line_model.objects.create(
slug='event-a-foo-bar',
@ -1487,6 +1489,7 @@ def test_detail_pool_invoices(app, admin_user, draft):
event_slug='agenda-a@event-a',
agenda_slug='agenda-a',
activity_label='Agenda A',
description='Sun04, Mon05',
)
if not draft:
payment1 = Payment.objects.create(
@ -2067,6 +2070,7 @@ def test_invoice_pdf(app, admin_user, draft):
event_slug='agenda-1@event-1',
agenda_slug='agenda-1',
activity_label='Agenda 1',
description='Thu01, Fri02, Sat03',
)
line_model.objects.create(
event_date=datetime.date(2022, 9, 2),

View File

@ -776,6 +776,7 @@ def test_regie_invoices(app, admin_user, orphan):
event_slug='agenda-a@event-a',
agenda_slug='agenda-a',
activity_label='Agenda A',
description='Thu01, Fri02, Sat03',
)
invoice_line12 = InvoiceLine.objects.create(
slug='event-b-foo-bar',
@ -800,6 +801,7 @@ def test_regie_invoices(app, admin_user, orphan):
event_slug='agenda-b@event-b',
agenda_slug='agenda-b',
activity_label='Agenda B',
description='Thu01, Fri02, Sat03',
)
invoice_line13 = InvoiceLine.objects.create(
slug='event-a-foo-bar',
@ -821,6 +823,7 @@ def test_regie_invoices(app, admin_user, orphan):
event_slug='agenda-a@event-a',
agenda_slug='agenda-a',
activity_label='Agenda A',
description='Sun04, Mon05',
)
payment1 = Payment.objects.create(
regie=regie,
@ -1388,6 +1391,7 @@ def test_regie_invoice_pdf(app, admin_user):
event_slug='agenda-1@event-1',
agenda_slug='agenda-1',
activity_label='Agenda 1',
description='Thu01, Fri02, Sat03',
)
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 2),
@ -1411,7 +1415,6 @@ def test_regie_invoice_pdf(app, admin_user):
user_first_name='User1',
user_last_name='Name1',
)
Agenda.objects.create(label='Agenda 2', regie=regie, partial_bookings=True)
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
invoice=invoice,
@ -1426,6 +1429,7 @@ def test_regie_invoice_pdf(app, admin_user):
'agenda': 'agenda-2',
'primary_event': 'event-1',
'status': 'presence',
'partial_bookings': True,
},
event_slug='agenda-2@event-1',
agenda_slug='agenda-2',
@ -1445,6 +1449,7 @@ def test_regie_invoice_pdf(app, admin_user):
'agenda': 'agenda-2',
'primary_event': 'event-1',
'status': 'absence',
'partial_bookings': True,
},
event_slug='agenda-2@event-1',
agenda_slug='agenda-2',

View File

@ -2623,10 +2623,12 @@ def test_generate_invoices_from_lines_aggregation():
'check_type_label': 'Foo!',
'dates': ['2022-09-01', '2022-09-02', '2022-09-03'],
'event_time': '12:00:00',
'partial_bookings': False,
}
assert iline1.event_slug == 'agenda-1@event-1'
assert iline1.agenda_slug == 'agenda-1'
assert iline1.activity_label == 'Agenda 1'
assert iline1.description == 'Thu01, Fri02, Sat03'
assert iline1.user_external_id == 'user:1'
assert iline1.user_first_name == 'UserFirst1'
assert iline1.user_last_name == 'UserLast1'
@ -2655,10 +2657,12 @@ def test_generate_invoices_from_lines_aggregation():
'check_type_label': 'Foo!',
'dates': ['2022-09-01', '2022-09-02', '2022-09-03'],
'event_time': '12:00:00',
'partial_bookings': False,
}
assert iline2.event_slug == 'agenda-1@event-1'
assert iline2.agenda_slug == 'agenda-1'
assert iline2.activity_label == 'Agenda 1'
assert iline2.description == 'Thu01, Fri02, Sat03'
assert iline2.user_external_id == 'user:1'
assert iline2.user_first_name == 'UserFirst1'
assert iline2.user_last_name == 'UserLast1'
@ -2709,10 +2713,12 @@ def test_generate_invoices_from_lines_aggregation():
'check_type_label': 'Foo!',
'dates': ['2022-09-01'],
'event_time': '12:00:00',
'partial_bookings': False,
}
assert iline3.event_slug == 'agenda-1@event-1'
assert iline3.agenda_slug == 'agenda-1'
assert iline3.activity_label == 'Agenda 1'
assert iline3.description == 'Thu01'
assert iline4.details == {
'agenda': 'agenda-2',
'primary_event': 'event-1',
@ -2722,10 +2728,12 @@ def test_generate_invoices_from_lines_aggregation():
'check_type_label': 'Foo!',
'dates': ['2022-09-01'],
'event_time': '12:00:00',
'partial_bookings': False,
}
assert iline4.event_slug == 'agenda-2@event-1'
assert iline4.agenda_slug == 'agenda-2'
assert iline4.activity_label == 'Agenda 2'
assert iline4.description == 'Thu01'
assert iline5.details == {
'agenda': 'agenda-1',
'primary_event': 'event-2',
@ -2735,10 +2743,12 @@ def test_generate_invoices_from_lines_aggregation():
'check_type_label': 'Foo!',
'dates': ['2022-09-01'],
'event_time': '12:00:00',
'partial_bookings': False,
}
assert iline5.event_slug == 'agenda-1@event-2'
assert iline5.agenda_slug == 'agenda-1'
assert iline5.activity_label == 'Agenda 1'
assert iline5.description == 'Thu01'
assert iline6.details == {
'agenda': 'agenda-1',
'primary_event': 'event-1',
@ -2748,10 +2758,12 @@ def test_generate_invoices_from_lines_aggregation():
'check_type_label': 'bar',
'dates': ['2022-09-01'],
'event_time': '12:00:00',
'partial_bookings': False,
}
assert iline6.event_slug == 'agenda-1@event-1'
assert iline6.agenda_slug == 'agenda-1'
assert iline6.activity_label == 'Agenda 1'
assert iline6.description == 'Thu01'
assert iline7.details == {
'agenda': 'agenda-1',
'primary_event': 'event-1',
@ -2761,10 +2773,12 @@ def test_generate_invoices_from_lines_aggregation():
'check_type_label': 'foo',
'dates': ['2022-09-01'],
'event_time': '12:00:00',
'partial_bookings': False,
}
assert iline7.event_slug == 'agenda-1@event-1'
assert iline7.agenda_slug == 'agenda-1'
assert iline7.activity_label == 'Agenda 1'
assert iline7.description == 'Thu01'
assert iline8.details == {
'agenda': 'agenda-1',
'primary_event': 'event-1',
@ -2774,10 +2788,12 @@ def test_generate_invoices_from_lines_aggregation():
'check_type_label': 'foo',
'dates': ['2022-09-01'],
'event_time': '12:00:00',
'partial_bookings': False,
}
assert iline8.event_slug == 'agenda-1@event-1'
assert iline8.agenda_slug == 'agenda-1'
assert iline8.activity_label == 'Agenda 1'
assert iline8.description == 'Thu01'
assert iline9.details == {
'agenda': 'agenda-1',
'primary_event': 'event-1',
@ -2787,18 +2803,22 @@ def test_generate_invoices_from_lines_aggregation():
'check_type_label': 'Foo!',
'dates': ['2022-09-01'],
'event_time': '12:00:00',
'partial_bookings': False,
}
assert iline9.event_slug == 'agenda-1@event-1'
assert iline9.agenda_slug == 'agenda-1'
assert iline9.activity_label == 'Agenda 1'
assert iline9.description == 'Thu01'
assert iline10.details == {}
assert iline10.event_slug == 'agenda-1@foobar'
assert iline10.agenda_slug == 'agenda-1'
assert iline10.activity_label == 'Agenda 1'
assert iline10.description == ''
assert iline11.details == {}
assert iline11.event_slug == 'foobar'
assert iline11.agenda_slug == ''
assert iline11.activity_label == ''
assert iline11.description == ''
@mock.patch('lingo.invoicing.models.lock_events_check')

View File

@ -1,5 +1,4 @@
import pytest
from django.template import Context, Template
from .utils import login
@ -33,13 +32,3 @@ def test_menu_json(app, admin_user):
resp = app.get('/manage/menu.json?callback=fooBar')
assert resp.headers['content-type'] == 'application/javascript'
assert resp.text.startswith('fooBar([{"')
def test_parse_date_templatetag():
tmpl = Template('{% load lingo %}{{ plop|parse_date|date:"d" }}')
assert tmpl.render(Context({'plop': '2017-12-21'})) == '21'
assert tmpl.render(Context({'plop': 'x'})) == ''
assert tmpl.render(Context({'plop': None})) == ''
assert tmpl.render(Context({'plop': 3})) == ''
assert tmpl.render(Context({'plop': {'foo': 'bar'}})) == ''
assert tmpl.render(Context({})) == ''