invoicing: display and filter cancelled credits (#89810)
This commit is contained in:
parent
73f2cafd40
commit
9352f2d0c7
|
@ -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
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue