invoicing: display and filter cancelled credits (#89810)

This commit is contained in:
Lauréline Guérin 2024-04-23 10:59:08 +02:00
parent 73f2cafd40
commit 9352f2d0c7
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
5 changed files with 175 additions and 88 deletions

View File

@ -1080,6 +1080,16 @@ class RegieCreditFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet)
],
method='filter_assigned',
)
cancelled = django_filters.ChoiceFilter(
label=_('Cancelled'),
widget=forms.RadioSelect,
empty_label=_('all'),
choices=[
('yes', _('Yes')),
('no', _('No')),
],
method='filter_cancelled',
)
agenda = django_filters.ChoiceFilter(
label=_('Activity'),
empty_label=_('all'),
@ -1137,6 +1147,13 @@ class RegieCreditFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet)
return queryset.filter(assigned_amount=0)
return queryset
def filter_cancelled(self, queryset, name, value):
if not value:
return queryset
if value == 'yes':
return queryset.filter(cancelled_at__isnull=False)
return queryset.filter(cancelled_at__isnull=True)
def filter_agenda(self, queryset, name, value):
if not value:
return queryset

View File

@ -1067,6 +1067,19 @@ class InvoiceCancellationReason(models.Model):
return slugify(self.label)
def get_cancellation_info(obj):
result = []
if not obj.cancelled_at:
return result
result.append((_('Cancelled on'), obj.cancelled_at.strftime('%d/%m/%Y %H:%M')))
if obj.cancelled_by:
result.append((_('Cancelled by'), obj.cancelled_by))
result.append((_('Reason'), obj.cancellation_reason))
if obj.cancellation_description:
result.append((_('Description'), linebreaksbr(obj.cancellation_description)))
return result
class Invoice(AbstractInvoice):
number = models.PositiveIntegerField(default=0)
formatted_number = models.CharField(max_length=200)
@ -1135,16 +1148,7 @@ class Invoice(AbstractInvoice):
return sorted(invoice_payments.values(), key=lambda a: a.payment.created_at)
def get_cancellation_info(self):
result = []
if not self.cancelled_at:
return result
result.append((_('Cancelled on'), self.cancelled_at.strftime('%d/%m/%Y %H:%M')))
if self.cancelled_by:
result.append((_('Cancelled by'), self.cancelled_by))
result.append((_('Reason'), self.cancellation_reason))
if self.cancellation_description:
result.append((_('Description'), linebreaksbr(self.cancellation_description)))
return result
return get_cancellation_info(self)
class Credit(AbstractInvoiceObject):
@ -1222,6 +1226,9 @@ class Credit(AbstractInvoiceObject):
}
return template.render(context)
def get_cancellation_info(self):
return get_cancellation_info(self)
class AbstractInvoiceLineObject(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False, null=True)
@ -1610,16 +1617,7 @@ class Payment(models.Model):
return result
def get_cancellation_info(self):
result = []
if not self.cancelled_at:
return result
result.append((_('Cancelled on'), self.cancelled_at.strftime('%d/%m/%Y %H:%M')))
if self.cancelled_by:
result.append((_('Cancelled by'), self.cancelled_by))
result.append((_('Reason'), self.cancellation_reason))
if self.cancellation_description:
result.append((_('Description'), linebreaksbr(self.cancellation_description)))
return result
return get_cancellation_info(self)
def get_invoice_payments(self):
if hasattr(self, 'prefetched_invoicelinepayments'):

View File

@ -44,62 +44,73 @@
<td></td>
</tr>
{% endfor %}
<tr class="line" data-related-invoicing-element-id="{{ credit.pk }}">
<th colspan="7" class="assignments">
{% trans "Assignments" %}
</th>
</tr>
<tr class="line" data-related-invoicing-element-id="{{ credit.pk }}">
<td class="payment-num">{% trans "Payment" context 'payment' %}</td>
<td colspan="4">{% trans "Date" context 'payment' %}</td>
<td class="amount" colspan="2">{% trans "Amount" %}</td>
</tr>
{% for assignment in credit.creditassignment_set.all %}
{% if not credit.cancelled_at %}
<tr class="line" data-related-invoicing-element-id="{{ credit.pk }}">
{% if assignment.payment %}
<td>
<a href="{% url 'lingo-manager-invoicing-regie-payment-list' regie_pk=regie.pk %}?number={{ assignment.payment.formatted_number }}">{{ assignment.payment.formatted_number }}</a>
</td>
<td colspan="4">
{{ assignment.payment.created_at|date:"DATETIME_FORMAT" }}
</td>
{% elif assignment.invoice %}
<td>
<i>{% trans "Pending..." %}</i>
</td>
<td colspan="4"></td>
{% else %}
<td>
<a href="{% url 'lingo-manager-invoicing-regie-refund-list' regie_pk=regie.pk %}?number={{ assignment.refund.formatted_number }}">{{ assignment.refund.formatted_number }} ({% trans "Refund" %})</a>
</td>
<td colspan="4">
{{ assignment.refund.created_at|date:"DATETIME_FORMAT" }}
</td>
{% endif %}
<td class="amount" colspan="2">
{% blocktrans with amount=assignment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}
</td>
<th colspan="7" class="assignments">
{% trans "Assignments" %}
</th>
</tr>
{% empty %}
<tr class="line" data-related-invoicing-element-id="{{ credit.pk }}">
<td colspan="7" class="no-payments">
{% trans "No assignments for this credit" %}
</td>
</tr>
{% endfor %}
{% if credit.assigned_amount %}
<tr class="line {% if not credit.remaining_amount%}last-line{% endif %}" data-related-invoicing-element-id="{{ credit.pk }}">
<td colspan="2"></td>
<td class="invoice-details" colspan="5">
<i>{% trans "Assigned amount:" %} {% blocktrans with amount=credit.assigned_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</i>
</td>
</tr>
{% endif %}
{% if credit.remaining_amount %}
<tr class="line last-line" data-related-invoicing-element-id="{{ credit.pk }}">
<td colspan="2"></td>
<td class="invoice-details" colspan="5">
<i>{% trans "Remaining amount to assign:" %} {% blocktrans with amount=credit.remaining_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</i>
</td>
<td class="payment-num">{% trans "Payment" context 'payment' %}</td>
<td colspan="4">{% trans "Date" context 'payment' %}</td>
<td class="amount" colspan="2">{% trans "Amount" %}</td>
</tr>
{% for assignment in credit.creditassignment_set.all %}
<tr class="line" data-related-invoicing-element-id="{{ credit.pk }}">
{% if assignment.payment %}
<td>
<a href="{% url 'lingo-manager-invoicing-regie-payment-list' regie_pk=regie.pk %}?number={{ assignment.payment.formatted_number }}">{{ assignment.payment.formatted_number }}</a>
</td>
<td colspan="4">
{{ assignment.payment.created_at|date:"DATETIME_FORMAT" }}
</td>
{% elif assignment.invoice %}
<td>
<i>{% trans "Pending..." %}</i>
</td>
<td colspan="4"></td>
{% else %}
<td>
<a href="{% url 'lingo-manager-invoicing-regie-refund-list' regie_pk=regie.pk %}?number={{ assignment.refund.formatted_number }}">{{ assignment.refund.formatted_number }} ({% trans "Refund" %})</a>
</td>
<td colspan="4">
{{ assignment.refund.created_at|date:"DATETIME_FORMAT" }}
</td>
{% endif %}
<td class="amount" colspan="2">
{% blocktrans with amount=assignment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}
</td>
</tr>
{% empty %}
<tr class="line" data-related-invoicing-element-id="{{ credit.pk }}">
<td colspan="7" class="no-payments">
{% trans "No assignments for this credit" %}
</td>
</tr>
{% endfor %}
{% if credit.assigned_amount %}
<tr class="line {% if not credit.remaining_amount%}last-line{% endif %}" data-related-invoicing-element-id="{{ credit.pk }}">
<td colspan="2"></td>
<td class="invoice-details" colspan="5">
<i>{% trans "Assigned amount:" %} {% blocktrans with amount=credit.assigned_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</i>
</td>
</tr>
{% endif %}
{% if credit.remaining_amount %}
<tr class="line last-line" data-related-invoicing-element-id="{{ credit.pk }}">
<td colspan="2"></td>
<td class="invoice-details" colspan="5">
<i>{% trans "Remaining amount to assign:" %} {% blocktrans with amount=credit.remaining_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</i>
</td>
</tr>
{% endif %}
{% else %}
{% for label, value in credit.get_cancellation_info %}
<tr class="line {% if forloop.last %}last-line{% endif %}" data-related-invoicing-element-id="{{ credit.pk }}">
<td colspan="2"></td>
<td class="invoice-details" colspan="5">
<i>{% blocktrans %}{{ label }}:{% endblocktrans %} {{ value }}</i>
</td>
</tr>
{% endfor %}
{% endif %}

View File

@ -31,7 +31,9 @@
{% for credit in object_list %}
<tr class="credit untoggled" data-invoicing-element-id="{{ credit.pk }}" data-invoicing-element-lines-url="{% url 'lingo-manager-invoicing-regie-credit-line-list' regie_pk=regie.pk credit_pk=credit.pk %}">
<td colspan="6">
{% if credit.remaining_amount > 0 and credit.assigned_amount > 0 %}
{% if credit.cancelled_at %}
<span class="meta meta-error">{% trans "Cancelled" context "credit" %}</span>
{% elif credit.remaining_amount > 0 and credit.assigned_amount > 0 %}
<span class="meta meta-warning">{% trans "Partially assigned" %}</span>
{% elif credit.remaining_amount == 0 %}
<span class="meta meta-success">{% trans "Assigned" %}</span>

View File

@ -13,6 +13,7 @@ from lingo.invoicing.models import (
CreditAssignment,
CreditLine,
Invoice,
InvoiceCancellationReason,
Payment,
PaymentType,
Refund,
@ -64,6 +65,20 @@ def test_regie_credits(app, admin_user):
)
credit3.set_number()
credit3.save()
credit4 = Credit.objects.create(
date_publication=datetime.date(2022, 10, 1),
regie=regie,
payer_external_id='payer:3',
payer_first_name='First3',
payer_last_name='Name3',
payer_address='43 rue des kangourous\n99999 Kangourou Ville',
cancelled_at=now(),
cancelled_by=admin_user,
cancellation_reason=InvoiceCancellationReason.objects.create(label='Final pool deletion'),
cancellation_description='foo bar\nblah',
)
credit4.set_number()
credit4.save()
CreditLine.objects.create(
slug='event-a-foo-bar',
@ -178,6 +193,21 @@ def test_regie_credits(app, admin_user):
assert credit3.remaining_amount == 1
assert credit3.assigned_amount == 0
CreditLine.objects.create(
slug='injected',
event_date=datetime.date(2022, 9, 1),
credit=credit4,
quantity=1,
unit_amount=1,
label='Event A',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
credit4.refresh_from_db()
assert credit4.remaining_amount == 1
assert credit4.assigned_amount == 0
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/credits/' % regie.pk)
assert resp.pyquery(
@ -294,31 +324,58 @@ def test_regie_credits(app, admin_user):
'Remaining amount to assign: 1.00€',
]
assert resp.pyquery(
'tr[data-invoicing-element-id="%s"]' % credit4.pk
).text() == 'Cancelled Credit A%02d-%s-0000004 dated %s for First3 Name3, amount 1.00€ - download' % (
regie.pk,
credit4.created_at.strftime('%y-%m'),
credit4.created_at.strftime('%d/%m/%Y'),
)
assert len(resp.pyquery('tr[data-invoicing-element-id="%s"] a' % credit4.pk)) == 2
lines_url = resp.pyquery('tr[data-invoicing-element-id="%s"]' % credit4.pk).attr(
'data-invoicing-element-lines-url'
)
assert lines_url == '/manage/invoicing/ajax/regie/%s/credit/%s/lines/' % (
regie.pk,
credit4.pk,
)
lines_resp = app.get(lines_url)
assert len(lines_resp.pyquery('tr')) == 7
assert [PyQuery(tr).text() for tr in lines_resp.pyquery('tr')] == [
'User1 Name1',
'Description\nAccounting code\nAmount\nQuantity\nSubtotal',
'Event A\n1.00€\n1\n1.00€',
'Cancelled on: %s' % credit4.cancelled_at.strftime('%d/%m/%Y %H:%M'),
'Cancelled by: admin',
'Reason: Final pool deletion',
'Description: foo bar\nblah',
]
# test filters
today = now().date()
tomorrow = today + datetime.timedelta(days=1)
yesterday = today - datetime.timedelta(days=1)
params = [
({'number': credit1.formatted_number}, 1),
({'number': credit1.created_at.strftime('%y-%m')}, 3),
({'created_at_after': today.strftime('%Y-%m-%d')}, 3),
({'number': credit1.created_at.strftime('%y-%m')}, 4),
({'created_at_after': today.strftime('%Y-%m-%d')}, 4),
({'created_at_after': tomorrow.strftime('%Y-%m-%d')}, 0),
({'created_at_before': yesterday.strftime('%Y-%m-%d')}, 0),
({'created_at_before': today.strftime('%Y-%m-%d')}, 3),
({'created_at_before': today.strftime('%Y-%m-%d')}, 4),
({'payment_number': payment1.formatted_number}, 1),
({'payment_number': payment1.created_at.strftime('%y-%m')}, 1),
({'payer_external_id': 'payer:1'}, 1),
({'payer_external_id': 'payer:2'}, 1),
({'payer_first_name': 'first'}, 3),
({'payer_first_name': 'first'}, 4),
({'payer_first_name': 'first1'}, 1),
({'payer_last_name': 'name'}, 3),
({'payer_last_name': 'name'}, 4),
({'payer_last_name': 'name1'}, 1),
({'user_external_id': 'user:1'}, 3),
({'user_external_id': 'user:1'}, 4),
({'user_external_id': 'user:2'}, 1),
({'user_first_name': 'user'}, 3),
({'user_first_name': 'user'}, 4),
({'user_first_name': 'user2'}, 1),
({'user_last_name': 'name'}, 3),
({'user_last_name': 'name1'}, 3),
({'user_last_name': 'name'}, 4),
({'user_last_name': 'name1'}, 4),
(
{
'total_amount_min': '1',
@ -331,25 +388,25 @@ def test_regie_credits(app, admin_user):
'total_amount_min': '1',
'total_amount_min_lookup': 'gte',
},
3,
4,
),
(
{
'total_amount_max': '6.2',
'total_amount_max_lookup': 'lt',
},
2,
3,
),
(
{
'total_amount_max': '6.2',
'total_amount_max_lookup': 'lte',
},
3,
4,
),
({'assigned': 'yes'}, 1),
({'assigned': 'partially'}, 1),
({'assigned': 'no'}, 1),
({'assigned': 'no'}, 2),
({'agenda': 'agenda-a'}, 2),
({'agenda': 'agenda-b'}, 1),
({'event': 'agenda-a@event-a'}, 1),
@ -358,6 +415,8 @@ def test_regie_credits(app, admin_user):
({'accounting_code': '42'}, 0),
({'accounting_code': '424242'}, 2),
({'accounting_code': '424243'}, 1),
({'cancelled': 'yes'}, 1),
({'cancelled': 'no'}, 3),
]
for param, result in params:
resp = app.get(