invoicing: select a Card Model in Payer form (#78015)

This commit is contained in:
Lauréline Guérin 2023-06-12 16:45:42 +02:00
parent 739aaf17cd
commit 4f5cb4552c
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
6 changed files with 196 additions and 14 deletions

View File

@ -29,6 +29,7 @@ from lingo.invoicing.models import (
Payer,
Regie,
)
from lingo.utils.wcs import get_wcs_options
class ExportForm(forms.Form):
@ -445,7 +446,29 @@ class RegieInvoiceFilterSet(django_filters.FilterSet):
return queryset
class PayerForm(forms.ModelForm):
class NewPayerForm(forms.ModelForm):
carddef_reference = forms.ChoiceField(
label=_('Linked card model'),
required=False,
)
class Meta:
model = Payer
fields = [
'label',
'description',
'carddef_reference',
'payer_external_id_prefix',
'payer_external_id_template',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
card_models = get_wcs_options('/api/cards/@list')
self.fields['carddef_reference'].choices = [('', '-----')] + card_models
class PayerForm(NewPayerForm):
class Meta:
model = Payer
fields = [

View File

@ -35,6 +35,7 @@ from django.utils.translation import gettext_lazy as _
from lingo.agendas.chrono import ChronoError, lock_events_check
from lingo.agendas.models import Agenda
from lingo.utils.misc import LingoImportError, generate_slug
from lingo.utils.wcs import get_wcs_matching_card_model
class RegieImportError(Exception):
@ -98,6 +99,15 @@ class Payer(models.Model):
payer, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
return created, payer
@property
def carddef_name(self):
if not self.carddef_reference:
return
result = get_wcs_matching_card_model(self.carddef_reference)
if not result:
return
return result
class Regie(models.Model):
label = models.CharField(_('Label'), max_length=150)

View File

@ -38,7 +38,7 @@
<h3>{% trans "Parameters" %}</h3>
<ul>
<li>{% trans "Identifier:" %} {{ payer.slug }}</li>
<li>{% trans "Card Model:" %} <code>{{ payer.carddef_reference }}</code></li>
<li>{% trans "Card Model:" %} <code>{{ payer.carddef_name|default:'' }}</code></li>
<li>{% trans "Prefix for payer external id:" %} <pre>{{ payer.payer_external_id_prefix }}</pre></li>
<li>{% trans "Template for payer external id:" %} <pre>{{ payer.payer_external_id_template }}</pre></li>
</ul>

View File

@ -21,7 +21,7 @@ from django.http import HttpResponse
from django.urls import reverse
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from lingo.invoicing.forms import PayerForm, PayerMappingForm
from lingo.invoicing.forms import NewPayerForm, PayerForm, PayerMappingForm
from lingo.invoicing.models import Payer
@ -36,13 +36,7 @@ payers_list = PayersListView.as_view()
class PayerAddView(CreateView):
template_name = 'lingo/invoicing/manager_payer_form.html'
model = Payer
fields = [
'label',
'description',
'carddef_reference',
'payer_external_id_prefix',
'payer_external_id_template',
]
form_class = NewPayerForm
def get_success_url(self):
return reverse('lingo-manager-invoicing-payer-detail', args=[self.object.pk])

86
lingo/utils/wcs.py Normal file
View File

@ -0,0 +1,86 @@
# 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 json
from django.conf import settings
from requests.exceptions import RequestException
from lingo.utils import requests
def is_wcs_enabled(cls):
return hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('wcs')
def get_wcs_services():
if not is_wcs_enabled(None):
return {}
return settings.KNOWN_SERVICES.get('wcs')
def get_wcs_json(wcs_site, path, log_errors=True):
if wcs_site is None:
# no site specified (probably an import referencing a not yet deployed
# site)
return {'err': 1, 'err_desc': 'no-wcs-site'}
try:
response = requests.get(
path,
remote_service=wcs_site,
without_user=True,
headers={'accept': 'application/json'},
log_errors=log_errors,
)
response.raise_for_status()
except RequestException as e:
if e.response is not None:
try:
# return json if available (on 404 responses by example)
return e.response.json()
except json.JSONDecodeError:
pass
return {'err': 1, 'data': None}
return response.json()
def get_wcs_options(url):
references = []
for wcs_key, wcs_site in sorted(get_wcs_services().items(), key=lambda x: x[1]['title']):
site_title = wcs_site.get('title')
response_json = get_wcs_json(wcs_site, url)
if response_json.get('err') == 1:
continue
response_json = response_json.get('data')
if not response_json:
continue
for element in response_json:
slug = element.get('slug')
title = element.get('title')
if len(get_wcs_services()) == 1:
label = title
else:
label = '%s : %s' % (site_title, title)
reference = '%s:%s' % (wcs_key, slug)
references.append((reference, label))
return references
def get_wcs_matching_card_model(ref):
card_models = get_wcs_options('/api/cards/@list')
for carddef_reference, card_label in card_models:
if carddef_reference == ref:
return card_label

View File

@ -1,3 +1,8 @@
import copy
import json
import urllib.parse
from unittest import mock
import pytest
from lingo.invoicing.models import Payer, Regie
@ -6,14 +11,51 @@ from tests.utils import login
pytestmark = pytest.mark.django_db
def test_add_payer(app, admin_user):
WCS_CARDDEFS_DATA = [
{'title': 'Card Model 1', 'slug': 'card_model_1', 'custom_views': [{'id': 'foo', 'text': 'bar'}]},
{'title': 'Card Model 2', 'slug': 'card_model_2'},
{'title': 'Card Model 3', 'slug': 'card_model_3'},
]
class MockedRequestResponse(mock.Mock):
status_code = 200
def json(self):
return json.loads(self.content)
def get_data_from_url(url):
if '/api/cards/@list' in url:
return WCS_CARDDEFS_DATA
return []
def mocked_requests_send(request, **kwargs):
request_url = urllib.parse.urlparse(request.url)
data = copy.deepcopy(get_data_from_url(request_url.path))
if not isinstance(data, list):
return MockedRequestResponse(content=json.dumps(data))
return MockedRequestResponse(content=json.dumps({'data': data}))
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_add_payer(mock_send, app, admin_user):
app = login(app)
resp = app.get('/manage/invoicing/')
resp = resp.click('Payers')
resp = resp.click('New payer')
assert resp.context['form'].fields['carddef_reference'].widget.choices == [
('', '-----'),
('default:card_model_1', 'Card Model 1'),
('default:card_model_2', 'Card Model 2'),
('default:card_model_3', 'Card Model 3'),
]
resp.form['label'] = 'Foo bar'
resp.form['description'] = 'a little description'
resp.form['carddef_reference'] = 'foo'
resp.form['carddef_reference'] = 'default:card_model_1'
resp.form['payer_external_id_prefix'] = 'payer:'
resp.form['payer_external_id_template'] = 'bar'
resp = resp.form.submit()
@ -22,12 +64,13 @@ def test_add_payer(app, admin_user):
assert payer.label == 'Foo bar'
assert payer.slug == 'foo-bar'
assert payer.description == 'a little description'
assert payer.carddef_reference == 'foo'
assert payer.carddef_reference == 'default:card_model_1'
assert payer.payer_external_id_prefix == 'payer:'
assert payer.payer_external_id_template == 'bar'
def test_edit_payer(app, admin_user):
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_edit_payer(mock_send, app, admin_user):
payer = Payer.objects.create(label='Foo bar', carddef_reference='foo')
payer2 = Payer.objects.create(label='baz')
@ -35,8 +78,15 @@ def test_edit_payer(app, admin_user):
resp = app.get('/manage/invoicing/payers/')
resp = resp.click(href='/manage/invoicing/payer/%s/' % payer.pk)
resp = resp.click(href='/manage/invoicing/payer/%s/edit/' % payer.pk)
assert resp.context['form'].fields['carddef_reference'].widget.choices == [
('', '-----'),
('default:card_model_1', 'Card Model 1'),
('default:card_model_2', 'Card Model 2'),
('default:card_model_3', 'Card Model 3'),
]
resp.form['label'] = 'Foo bar baz'
resp.form['slug'] = payer2.slug
resp.form['carddef_reference'] = 'default:card_model_1'
resp = resp.form.submit()
assert resp.context['form'].errors['slug'] == ['Another payer exists with the same identifier.']
@ -46,6 +96,7 @@ def test_edit_payer(app, admin_user):
payer.refresh_from_db()
assert payer.label == 'Foo bar baz'
assert payer.slug == 'baz2'
assert payer.carddef_reference == 'default:card_model_1'
def test_edit_payer_mapping(app, admin_user):
@ -77,3 +128,21 @@ def test_delete_payer(app, admin_user):
resp = app.get('/manage/invoicing/payer/%s/' % payer.pk)
assert '/manage/invoicing/payer/%s/delete/' % payer.pk not in resp
resp = app.get('/manage/invoicing/payer/%s/delete/' % payer.pk, status=404)
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_detail_payer(mock_send, app, admin_user):
payer = Payer.objects.create(label='Foo bar')
app = login(app)
resp = app.get('/manage/invoicing/payer/%s/' % payer.pk)
assert 'Card Model: <code></code>' in resp
payer.carddef_reference = 'unknown'
payer.save()
resp = app.get('/manage/invoicing/payer/%s/' % payer.pk)
assert 'Card Model: <code></code>' in resp
payer.carddef_reference = 'default:card_model_1'
payer.save()
resp = app.get('/manage/invoicing/payer/%s/' % payer.pk)
assert 'Card Model: <code>Card Model 1</code>' in resp