basket: see basket details and temporary invoice (#83263)
gitea/lingo/pipeline/head This commit looks good Details

This commit is contained in:
Lauréline Guérin 2023-11-10 10:09:41 +01:00 committed by Lauréline Guérin
parent c7c1d8063c
commit 2927790445
9 changed files with 338 additions and 1 deletions

38
lingo/basket/apps.py Normal file
View File

@ -0,0 +1,38 @@
# 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/>.
import django.apps
from django.utils.translation import gettext_lazy as _
def user_get_name_id(user):
if not hasattr(user, '_name_id'):
user._name_id = None
saml_identifier = user.saml_identifiers.first()
if saml_identifier:
user._name_id = saml_identifier.name_id
return user._name_id
class AppConfig(django.apps.AppConfig):
name = 'lingo.basket'
verbose_name = _('Basket')
def ready(self):
from django.contrib.auth import get_user_model
get_user_model().add_to_class('get_name_id', user_get_name_id)

View File

@ -53,6 +53,9 @@ class Basket(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def get_lines(self):
return self.basketline_set.filter(closed=True).order_by('pk')
class BasketLine(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
@ -80,6 +83,11 @@ class BasketLine(models.Model):
class Meta:
unique_together = ('basket', 'user_external_id')
@property
def user_name(self):
user_name = '%s %s' % (self.user_first_name, self.user_last_name)
return user_name.strip()
def get_items(self):
if not self.group_items:
return self.get_items_without_grouping()

View File

@ -0,0 +1,36 @@
{% extends "lingo/interstitial_base.html" %}
{% load i18n %}
{% block title %}{% trans "Basket" %}{% endblock %}
{% block content %}
<main>
<p>
{% trans "My basket" %}
</p>
{% with lines=basket.get_lines %}
<ul class="basket">
{% for line in lines %}
{% for subject, quantity, unit_amount, total_amount in line.get_items %}
<li class="basket-item">
<a class="basket-item--label" {% if line.form_url %}href="{{ line.form_url }}{% endif %}">{{ line.user_name }} - {{ subject }}</a>
<span class="basket-item--total-amount">{% blocktrans with amount=total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</span>
</li>
{% endfor %}
{% endfor %}
</ul>
{% if lines %}
<a href="{% url 'lingo-basket-invoice-pdf' %}">{% trans "see invoice" %}</a>
{% endif %}
{% endwith %}
</main>
<style>
ul.basket {
list-style: none;
padding-left: 0;
}
li.basket-item {
text-align: left;
}
</style>
{% endblock %}

24
lingo/basket/urls.py Normal file
View File

@ -0,0 +1,24 @@
# 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.urls import path
from . import views
public_urlpatterns = [
path('', views.basket_detail, name='lingo-basket-detail'),
path('invoice/pdf/', views.basket_invoice_pdf, name='lingo-basket-invoice-pdf'),
]

52
lingo/basket/views.py Normal file
View File

@ -0,0 +1,52 @@
# 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.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
from django.views.generic import TemplateView, View
from lingo.basket.models import Basket
from lingo.invoicing.views.utils import PDFMixin
class BasketDetailView(LoginRequiredMixin, TemplateView):
template_name = 'lingo/basket/basket_detail.html'
def get_context_data(self, **kwargs):
nameid = self.request.user.get_name_id()
basket = Basket.objects.filter(
status__in=['open', 'tobepaid'],
payer_nameid=nameid,
).first()
kwargs['basket'] = basket
return super().get_context_data(**kwargs)
basket_detail = BasketDetailView.as_view()
class BasketInvoicePDFView(LoginRequiredMixin, PDFMixin, View):
def get_object(self):
nameid = self.request.user.get_name_id()
basket = get_object_or_404(
Basket,
status__in=['open', 'tobepaid'],
payer_nameid=nameid,
)
return basket.draft_invoice
basket_invoice_pdf = BasketInvoicePDFView.as_view()

View File

@ -20,6 +20,7 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path, re_path
from .api.urls import urlpatterns as lingo_api_urls
from .basket.urls import public_urlpatterns as lingo_basket_public_urls
from .epayment.urls import manager_urlpatterns as lingo_epayment_manager_urls
from .epayment.urls import public_urlpatterns as lingo_epayment_public_urls
from .invoicing.urls import urlpatterns as lingo_invoicing_urls
@ -34,6 +35,7 @@ urlpatterns = [
re_path(r'^manage/invoicing/', decorated_includes(manager_required, include(lingo_invoicing_urls))),
re_path(r'^manage/pricing/', decorated_includes(manager_required, include(lingo_pricing_urls))),
re_path(r'^manage/epayment/', decorated_includes(manager_required, include(lingo_epayment_manager_urls))),
path('basket/', include(lingo_basket_public_urls)),
path('api/', include(lingo_api_urls)),
path('login/', login, name='auth_login'),
path('logout/', logout, name='auth_logout'),

0
tests/basket/__init__.py Normal file
View File

175
tests/basket/test_basket.py Normal file
View File

@ -0,0 +1,175 @@
import datetime
import uuid
import pytest
from pyquery import PyQuery
from lingo.basket.models import Basket, BasketLine, BasketLineItem
from lingo.invoicing.models import DraftInvoice, Regie
from tests.utils import login
pytestmark = pytest.mark.django_db
def test_basket_detail(app, simple_user):
resp = app.get('/basket/')
assert resp.location.endswith('/login/?next=/basket/')
app = login(app, username='user', password='user')
# no basket object
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 0
assert '/basket/invoice/pdf/' not in resp
# basket without lines
regie = Regie.objects.create(label='Foo')
invoice = DraftInvoice.objects.create(
regie=regie,
date_publication=datetime.date(2023, 4, 21),
date_payment_deadline=datetime.date(2023, 4, 22),
date_due=datetime.date(2023, 4, 23),
)
basket = Basket.objects.create(regie=regie, draft_invoice=invoice, payer_nameid='ab' * 16)
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 0
assert '/basket/invoice/pdf/' not in resp
# a not closed line
line = BasketLine.objects.create(
basket=basket,
closed=False,
user_first_name='First1',
user_last_name='Last1',
)
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 0
assert '/basket/invoice/pdf/' not in resp
# line is closed but empty
line.closed = True
line.save()
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 0
assert '/basket/invoice/pdf/' in resp
# add some items, group_items is False
BasketLineItem.objects.create(
line=line,
label='Repas',
subject='Réservation',
details='Lun 06/11, Mar 07/11',
quantity=2,
unit_amount=3,
)
BasketLineItem.objects.create(
line=line,
label='Repas',
subject='Réservation',
details='Jeu 09/11',
quantity=1,
unit_amount=3,
)
BasketLineItem.objects.create(
line=line,
label='Repas',
subject='Annulation',
details='Ven 10/11',
quantity=-1,
unit_amount=3,
)
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 3
assert [PyQuery(li).text() for li in resp.pyquery('ul.basket li')] == [
'First1 Last1 - Repas - Annulation Ven 10/11 -3.00€',
'First1 Last1 - Repas - Réservation Jeu 09/11 3.00€',
'First1 Last1 - Repas - Réservation Lun 06/11, Mar 07/11 6.00€',
]
assert '/basket/invoice/pdf/' in resp
# group items
line.group_items = True
line.save()
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 2
assert [PyQuery(li).text() for li in resp.pyquery('ul.basket li')] == [
'First1 Last1 - Repas - Annulation Ven 10/11 -3.00€',
'First1 Last1 - Repas - Réservation Lun 06/11, Mar 07/11, Jeu 09/11 9.00€',
]
assert '/basket/invoice/pdf/' in resp
# not closed line
line.closed = False
line.save()
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 0
assert '/basket/invoice/pdf/' not in resp
# basket payer_nameid is wrong
line.closed = True
line.save()
basket.payer_nameid = uuid.uuid4()
basket.save()
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 0
assert '/basket/invoice/pdf/' not in resp
# check status
basket.payer_nameid = 'ab' * 16
basket.save()
for status in ['open', 'tobepaid']:
basket.status = status
basket.save()
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 2
for status in ['cancelled', 'expired', 'completed']:
basket.status = status
basket.save()
resp = app.get('/basket/')
assert 'My basket' in resp
assert len(resp.pyquery('ul.basket li')) == 0
def test_basket_invoice_pdf(app, simple_user):
resp = app.get('/basket/invoice/pdf/')
assert resp.location.endswith('/login/?next=/basket/invoice/pdf/')
app = login(app, username='user', password='user')
# no basket object
app.get('/basket/invoice/pdf/', status=404)
# basket
regie = Regie.objects.create(label='Foo')
invoice = DraftInvoice.objects.create(
regie=regie,
date_publication=datetime.date(2023, 4, 21),
date_payment_deadline=datetime.date(2023, 4, 22),
date_due=datetime.date(2023, 4, 23),
)
basket = Basket.objects.create(regie=regie, draft_invoice=invoice, payer_nameid='ab' * 16)
app.get('/basket/invoice/pdf/', status=200)
# basket payer_nameid is wrong
basket.payer_nameid = uuid.uuid4()
basket.save()
app.get('/basket/invoice/pdf/', status=404)
# check status
basket.payer_nameid = 'ab' * 16
basket.save()
for status in ['open', 'tobepaid']:
basket.status = status
basket.save()
app.get('/basket/invoice/pdf/', status=200)
for status in ['cancelled', 'expired', 'completed']:
basket.status = status
basket.save()
app.get('/basket/invoice/pdf/', status=404)

View File

@ -20,7 +20,9 @@ def app(request):
@pytest.fixture
def simple_user():
return User.objects.create_user('user', password='user')
user = User.objects.create_user('user', password='user')
user.saml_identifiers.create(name_id='ab' * 16)
return user
@pytest.fixture