basket: see basket details and temporary invoice (#83263)
gitea/lingo/pipeline/head This commit looks good
Details
gitea/lingo/pipeline/head This commit looks good
Details
This commit is contained in:
parent
c7c1d8063c
commit
2927790445
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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 %}
|
|
@ -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'),
|
||||
]
|
|
@ -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()
|
|
@ -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,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)
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue