Modèle de payeur (#78015) #60

Merged
lguerin merged 9 commits from wip/78015-invoicing-payer-model into main 2023-06-23 11:42:05 +02:00
36 changed files with 1558 additions and 795 deletions

View File

@ -27,8 +27,15 @@ from weasyprint import HTML
from lingo.agendas.models import Agenda
from lingo.api import serializers
from lingo.api.utils import APIErrorBadRequest, Response
from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, InjectedLine, Invoice, Payment, Regie
from lingo.pricing.models import AgendaPricing, PayerError
from lingo.invoicing.models import (
DraftInvoice,
DraftInvoiceLine,
InjectedLine,
Invoice,
PayerError,
Payment,
Regie,
)
class AgendaCheckTypeList(APIView):
@ -82,32 +89,22 @@ invoicing_regies = InvoicingRegies.as_view()
class InvoiceMixin:
def get_payer_external_ids(self, request, regie, nameid=None, payer_external_id=None):
def get_payer_external_id(self, request, regie, nameid=None, payer_external_id=None):
if payer_external_id:
return [payer_external_id]
return payer_external_id
if not nameid:
raise Http404
payer_external_ids = set()
agenda_pricing_qs = (
AgendaPricing.objects.filter(
agendas__regie=regie,
)
.distinct()
.order_by('pk')
)
for agenda_pricing in agenda_pricing_qs:
try:
payer_external_ids.add(agenda_pricing.get_payer_external_id_from_nameid(request, nameid))
except PayerError:
pass
return list(payer_external_ids)
try:
return regie.get_payer_external_id_from_nameid(request, nameid)
except PayerError:
raise Http404
class InvoicingInvoices(InvoiceMixin, APIView):
permission_classes = (permissions.IsAuthenticated,)
def get_invoices_queryset(self, request, regie):
payer_external_ids = self.get_payer_external_ids(
payer_external_id = self.get_payer_external_id(
request=request,
regie=regie,
nameid=request.GET.get('NameID'),
@ -117,7 +114,7 @@ class InvoicingInvoices(InvoiceMixin, APIView):
regie=regie,
remaining_amount__gt=0,
date_publication__lte=datetime.date.today(),
payer_external_id__in=payer_external_ids,
payer_external_id=payer_external_id,
).order_by('-created_at')
def get(self, request, regie_identifier):
@ -138,7 +135,7 @@ invoicing_invoices = InvoicingInvoices.as_view()
class InvoicingHistoryInvoices(InvoicingInvoices):
def get_invoices_queryset(self, request, regie):
payer_external_ids = self.get_payer_external_ids(
payer_external_id = self.get_payer_external_id(
request=request,
regie=regie,
nameid=request.GET.get('NameID'),
@ -148,7 +145,7 @@ class InvoicingHistoryInvoices(InvoicingInvoices):
regie=regie,
remaining_amount=0,
date_publication__lte=datetime.date.today(),
payer_external_id__in=payer_external_ids,
payer_external_id=payer_external_id,
).order_by('-created_at')
@ -160,7 +157,7 @@ class InvoicingInvoice(InvoiceMixin, APIView):
def get(self, request, regie_identifier, invoice_identifier):
regie = get_object_or_404(Regie, slug=regie_identifier)
payer_external_ids = self.get_payer_external_ids(
payer_external_id = self.get_payer_external_id(
request=request,
regie=regie,
nameid=request.GET.get('NameID'),
@ -171,7 +168,7 @@ class InvoicingInvoice(InvoiceMixin, APIView):
uuid=invoice_identifier,
regie=regie,
date_publication__lte=datetime.date.today(),
payer_external_id__in=payer_external_ids,
payer_external_id=payer_external_id,
)
return Response(
{'data': invoice.normalize(for_backoffice=bool(request.GET.get('payer_external_id')))}
@ -186,7 +183,7 @@ class InvoicingInvoicePDF(InvoiceMixin, APIView):
def get(self, request, regie_identifier, invoice_identifier):
regie = get_object_or_404(Regie, slug=regie_identifier)
payer_external_ids = self.get_payer_external_ids(
payer_external_id = self.get_payer_external_id(
request=request,
regie=regie,
nameid=request.GET.get('NameID'),
@ -197,7 +194,7 @@ class InvoicingInvoicePDF(InvoiceMixin, APIView):
uuid=invoice_identifier,
regie=regie,
date_publication__lte=datetime.date.today(),
payer_external_id__in=payer_external_ids,
payer_external_id=payer_external_id,
)
result = invoice.html()
html = HTML(string=result)

View File

@ -20,11 +20,21 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from gadjo.forms.widgets import MultiSelectWidget
from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Invoice, InvoiceLine, Regie
from lingo.invoicing.models import (
Campaign,
DraftInvoice,
DraftInvoiceLine,
Invoice,
InvoiceLine,
Payer,
Regie,
)
from lingo.utils.wcs import get_wcs_options
class ExportForm(forms.Form):
regies = forms.BooleanField(label=_('Regies'), required=False, initial=True)
payers = forms.BooleanField(label=_('Payers'), required=False, initial=True)
class ImportForm(forms.Form):
@ -34,7 +44,7 @@ class ImportForm(forms.Form):
class RegieForm(forms.ModelForm):
class Meta:
model = Regie
fields = ['label', 'slug', 'description', 'cashier_role', 'counter_name', 'number_format']
fields = ['label', 'slug', 'description', 'payer', 'cashier_role', 'counter_name', 'number_format']
def clean_slug(self):
slug = self.cleaned_data['slug']
@ -434,3 +444,68 @@ class RegieInvoiceFilterSet(django_filters.FilterSet):
if value == 'no':
return queryset.filter(paid_amount=0)
return queryset
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 = [
'label',
'slug',
'description',
'carddef_reference',
'payer_external_id_prefix',
'payer_external_id_template',
]
def clean_slug(self):
slug = self.cleaned_data['slug']
if Payer.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError(_('Another payer exists with the same identifier.'))
return slug
class PayerMappingForm(forms.ModelForm):
class Meta:
model = Payer
fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.cached_carddef_json:
return
for key, label in self.instance.user_variables:
self.fields[key] = forms.ChoiceField(
label=label,
choices=[('', '-----')] + [(k, v) for k, v in self.instance.carddef_fields.items()],
required=False,
)
def save(self):
self.instance.user_fields_mapping = {k: self.cleaned_data[k] for k, v in self.instance.user_variables}
self.instance.save()
return self.instance

View File

@ -0,0 +1,42 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0034_draft_invoice_uuid'),
]
operations = [
migrations.CreateModel(
name='Payer',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('label', models.CharField(max_length=150, verbose_name='Label')),
('slug', models.SlugField(max_length=160, unique=True, verbose_name='Identifier')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('carddef_reference', models.CharField(max_length=150, verbose_name='Card Model')),
('cached_carddef_json', models.JSONField(blank=True, default=dict)),
(
'payer_external_id_prefix',
models.CharField(blank=True, max_length=250, verbose_name='Prefix for payer external id'),
),
(
'payer_external_id_template',
models.CharField(
blank=True,
help_text='To get payer external id from user external id',
max_length=1000,
verbose_name='Template for payer external id',
),
),
('user_fields_mapping', models.JSONField(blank=True, default=dict)),
],
),
migrations.AlterModelOptions(
name='payer',
options={'ordering': ['label']},
),
]

View File

@ -0,0 +1,18 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0035_payer'),
]
operations = [
migrations.AddField(
model_name='regie',
name='payer',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='invoicing.payer'
),
),
]

View File

@ -26,6 +26,7 @@ from django.contrib.auth.models import Group
from django.core import validators
from django.core.serializers.json import DjangoJSONEncoder
from django.db import connection, models, transaction
from django.template import RequestContext, Template, TemplateSyntaxError, VariableDoesNotExist
from django.template.loader import get_template
from django.utils.formats import date_format
from django.utils.text import slugify
@ -34,7 +35,8 @@ 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 generate_slug
from lingo.utils.misc import LingoImportError, generate_slug
from lingo.utils.wcs import get_wcs_json, get_wcs_matching_card_model, get_wcs_services
class RegieImportError(Exception):
@ -46,6 +48,198 @@ class PoolPromotionError(Exception):
self.msg = msg
class InvoicingError(Exception):
def __init__(self, details=None):
self.details = details or {}
super().__init__()
class PayerError(InvoicingError):
pass
class PayerDataError(InvoicingError):
pass
class Payer(models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
description = models.TextField(_('Description'), null=True, blank=True)
carddef_reference = models.CharField(_('Card Model'), max_length=150)
cached_carddef_json = models.JSONField(blank=True, default=dict)
payer_external_id_prefix = models.CharField(
_('Prefix for payer external id'),
max_length=250,
blank=True,
)
payer_external_id_template = models.CharField(
_('Template for payer external id'),
max_length=1000,
help_text=_('To get payer external id from user external id'),
blank=True,
)
user_fields_mapping = models.JSONField(blank=True, default=dict)
class Meta:
ordering = ['label']
def __str__(self):
return self.label
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug(self)
super().save(*args, **kwargs)
if 'update_fields' in kwargs:
# don't populate the cache
return
def populate_cache():
if self.carddef_reference:
parts = self.carddef_reference.split(':')
wcs_key, card_slug = parts[:2]
wcs_site = get_wcs_services().get(wcs_key)
card_schema = get_wcs_json(wcs_site, 'api/cards/%s/@schema' % card_slug, log_errors='warn')
if not card_schema:
return
if card_schema.get('err') == 1:
return
self.cached_carddef_json = card_schema
self.save(update_fields=['cached_carddef_json'])
populate_cache()
@property
def base_slug(self):
return slugify(self.label)
def export_json(self):
return {
'label': self.label,
'slug': self.slug,
'description': self.description,
'carddef_reference': self.carddef_reference,
'payer_external_id_prefix': self.payer_external_id_prefix,
'payer_external_id_template': self.payer_external_id_template,
'user_fields_mapping': self.user_fields_mapping,
}
@classmethod
def import_json(cls, data):
data = copy.deepcopy(data)
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
@property
def carddef_fields(self):
if not self.cached_carddef_json:
return
return {f['varname']: f['label'] for f in self.cached_carddef_json.get('fields') if f['varname']}
@property
def user_variables(self):
return [
('first_name', _('First name')),
('last_name', _('Last name')),
('demat', _('Demat')),
('direct_debit', _('Direct debit')),
]
@property
def user_fields(self):
result = []
for key, label in self.user_variables:
value = ''
if self.user_fields_mapping.get(key):
varname = self.user_fields_mapping.get(key)
value = self.carddef_fields.get(varname) or ''
result.append((label, value))
return result
def get_payer_external_id(self, request, original_context):
context = RequestContext(request)
context.push(original_context)
tplt = self.payer_external_id_template or ''
if not tplt:
raise PayerError(details={'reason': 'empty-template'})
try:
value = Template(tplt).render(context)
if not value:
raise PayerError(details={'reason': 'empty-result'})
return '%s%s' % (self.payer_external_id_prefix, value)
except TemplateSyntaxError:
raise PayerError(details={'reason': 'syntax-error'})
except VariableDoesNotExist:
raise PayerError(details={'reason': 'variable-error'})
def get_payer_external_id_from_nameid(self, request, original_context):
if not self.carddef_reference:
raise PayerError(details={'reason': 'missing-card-model'})
context = RequestContext(request)
context.push(original_context)
tplt = (
'{{ cards|objects:"%s"|filter_by_user:nameid|first|get:"id"|default:"" }}'
% self.carddef_reference.split(':')[1]
)
value = Template(tplt).render(context)
if not value:
raise PayerError(details={'reason': 'empty-result'})
return '%s%s' % (self.payer_external_id_prefix, value)
def get_payer_data(self, request, payer_external_id):
if not self.carddef_reference:
raise PayerError(details={'reason': 'missing-card-model'})
result = {}
context = RequestContext(request)
payer_external_raw_id = None
if ':' in payer_external_id:
payer_external_raw_id = payer_external_id.split(':')[1]
context.push({'payer_external_id': payer_external_raw_id or payer_external_id})
bool_keys = ['demat', 'direct_debit']
for key, dummy in self.user_variables:
if not self.user_fields_mapping.get(key):
if key not in bool_keys:
raise PayerDataError(details={'key': key, 'reason': 'not-defined'})
tplt = 'False'
else:
tplt = (
'{{ cards|objects:"%s"|filter_by_internal_id:payer_external_id|include_fields|first|get:"fields"|get:"%s"|default:"" }}'
% (
self.carddef_reference.split(':')[1],
self.user_fields_mapping[key],
)
)
value = Template(tplt).render(context)
if not value:
if key not in bool_keys:
raise PayerDataError(details={'key': key, 'reason': 'empty-result'})
value = False
if key in bool_keys:
if value in ('True', 'true', '1'):
value = True
elif value in ('False', 'false', '0'):
value = False
else:
raise PayerDataError(details={'key': key, 'reason': 'not-a-boolean'})
result[key] = value
return result
class Regie(models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
@ -61,6 +255,7 @@ class Regie(models.Model):
verbose_name=_('Cashier Role'),
on_delete=models.SET_NULL,
)
payer = models.ForeignKey(Payer, on_delete=models.PROTECT, blank=True, null=True)
counter_name = models.CharField(
_('Counter name'),
@ -96,6 +291,7 @@ class Regie(models.Model):
'permissions': {
'cashier': self.cashier_role.name if self.cashier_role else None,
},
'payer': self.payer.slug if self.payer else None,
}
@classmethod
@ -110,6 +306,11 @@ class Regie(models.Model):
raise RegieImportError('Missing role: %s' % role_name)
except Group.MultipleObjectsReturned:
raise RegieImportError('Multiple role exist with the name: %s' % role_name)
if data['payer']:
try:
data['payer'] = Payer.objects.get(slug=data['payer'])
except Payer.DoesNotExist:
raise LingoImportError(_('Missing "%s" payer') % data['payer'])
regie, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
return created, regie
@ -130,6 +331,25 @@ class Regie(models.Model):
regie_id=self.pk,
)
def get_payer_external_id(self, request, user_external_id):
if not self.payer:
raise PayerError(details={'reason': 'missing-payer'})
context = {'user_external_id': user_external_id}
if ':' in user_external_id:
context['user_external_raw_id'] = user_external_id.split(':')[1]
return self.payer.get_payer_external_id(request, context)
def get_payer_external_id_from_nameid(self, request, nameid):
if not self.payer:
raise PayerError(details={'reason': 'missing-payer'})
context = {'nameid': nameid}
return self.payer.get_payer_external_id_from_nameid(request, context)
def get_payer_data(self, request, payer_external_id):
if not self.payer:
raise PayerError(details={'reason': 'missing-payer'})
return self.payer.get_payer_data(request, payer_external_id)
class Campaign(models.Model):
label = models.CharField(_('Label'), max_length=150)

View File

@ -24,6 +24,10 @@
{% trans "Regies" %}
<p>{% trans "Invoicing regies." %}</p>
</a>
<a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-payer-list' %}">
{% trans "Payers" %}
<p>{% trans "Define here how payers are determined." %}</p>
</a>
</div>
</div>
{% endblock %}

View File

@ -1,6 +1,6 @@
{% load i18n %}
{% for line in object_list %}
<li class="line" data-related-invoice-id="{{ invoice.pk }}">
<li class="line" data-related-invoice-id="{{ line.invoice_id }}">
{% if line.pool %}
{% url 'lingo-manager-invoicing-pool-journal' regie_pk=regie.pk pk=line.pool.campaign_id pool_pk=line.pool.pk as journal_url %}
<a href="{{ journal_url }}?pk={{ line.pk }}">#{{ line.pk }}</a>

View File

@ -0,0 +1,88 @@
{% extends "lingo/invoicing/manager_payer_list.html" %}
{% load i18n gadjo %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-payer-detail' payer.pk %}">{{ payer }}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Payer' %} - {{ payer }}</h2>
<span class="actions">
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
<li><a href="{% url 'lingo-manager-invoicing-payer-edit' pk=payer.pk %}" rel="popup">{% trans "Edit" %}</a></li>
<li><a href="{% url 'lingo-manager-invoicing-payer-export' pk=payer.pk %}">{% trans 'Export' %}</a></li>
{% if not regies %}
<li><a href="{% url 'lingo-manager-invoicing-payer-delete' pk=payer.pk %}" rel="popup">{% trans "Delete" %}</a></li>
{% endif %}
</ul>
</span>
{% endblock %}
{% block content %}
{% with carddef_name=payer.carddef_name|default:'' %}
<div class="section">
<div class="pk-tabs">
<div class="pk-tabs--tab-list" role="tablist">
<button aria-controls="panel-settings" aria-selected="true" id="tab-settings" role="tab" tabindex="0">{% trans "Settings" %}</button>
{% if carddef_name %}
<button aria-controls="panel-mapping" aria-selected="false" id="tab-mapping" role="tab" tabindex="-1">{% trans "Mapping" %}</button>
{% endif %}
<button aria-controls="panel-usage" aria-selected="false" id="tab-usage" role="tab" tabindex="-1">{% trans "Used in regies" %}</button>
</div>
<div class="pk-tabs--container">
<div aria-labelledby="tab-settings" id="panel-settings" role="tabpanel" tabindex="0">
{% if payer.description %}
<h3>{% trans "Description" %}</h3>
<p>{{ payer.description|linebreaksbr }}</p>
{% endif %}
<h3>{% trans "Parameters" %}</h3>
<ul>
<li>{% trans "Identifier:" %} {{ payer.slug }}</li>
<li>{% trans "Card Model:" %} <code>{{ carddef_name }}</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>
</div>
{% if carddef_name %}
<div aria-labelledby="tab-mapping" hidden="" id="panel-mapping" role="tabpanel" tabindex="0">
<dl>
{% for label, value in payer.user_fields %}
<dt><b>{% blocktrans %}{{ label }}:{% endblocktrans %}</b></dt>
<dd><pre>{{ value }}</pre></dd>
{% endfor %}
</dl>
<div class="panel--buttons">
<a class="pk-button" href="{% url 'lingo-manager-invoicing-payer-edit-mapping' pk=payer.pk %}" rel="popup">{% trans "Edit mapping" %}</a>
</div>
</div>
{% endif %}
<div aria-labelledby="tab-usage" hidden="" id="panel-usage" role="tabpanel" tabindex="0">
{% if regies %}
<ul class="objects-list single-links">
{% for regie in regies %}
<li>
<a href="{% url 'lingo-manager-invoicing-regie-detail' pk=regie.pk %}">
{{ regie.label }}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="big-msg-info">
{% blocktrans trimmed %}
This Payer is not used yet.
{% endblocktrans %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends "lingo/invoicing/manager_payer_list.html" %}
{% load i18n gadjo %}
{% block breadcrumb %}
{{ block.super }}
{% if object.pk %}
<a href="{% url 'lingo-manager-invoicing-payer-detail' payer.pk %}">{{ payer }}</a>
<a href="{% url 'lingo-manager-invoicing-payer-edit' payer.pk %}">{% trans "Edit" %}</a>
{% else %}
<a href="{% url 'lingo-manager-invoicing-payer-add' %}">{% trans "New payer" %}</a>
{% endif %}
{% endblock %}
{% block appbar %}
{% if payer.pk %}
<h2>{% trans "Edit payer" %} - {{ payer }}</h2>
{% else %}
<h2>{% trans "New payer" %}</h2>
{% endif %}
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|with_template }}
<div class="buttons">
<button>{% trans "Submit" %}</button>
{% if object.pk %}
<a class="cancel" href="{% url 'lingo-manager-invoicing-payer-detail' payer.pk %}">{% trans 'Cancel' %}</a>
{% else %}
<a class="cancel" href="{% url 'lingo-manager-invoicing-payer-list' %}">{% trans 'Cancel' %}</a>
{% endif %}
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends "lingo/invoicing/manager_home.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-payer-list' %}">{% trans "Payers" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Payers' %}</h2>
<span class="actions">
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
<li>
<a href="{# url 'lingo-manager-invoicing-payer-import' #}">{% trans 'Import' %}</a>
</li>
<li>
<a href="{# url 'lingo-manager-invoicing-payer-export' #}">{% trans 'Export' %}</a>
</li>
</ul>
<a rel="popup" href="{% url 'lingo-manager-invoicing-payer-add' %}">{% trans 'New payer' %}</a>
</span>
{% endblock %}
{% block content %}
{% if object_list %}
<div>
<ul class="objects-list single-links">
{% for payer in object_list %}
<li>
<a href="{% url 'lingo-manager-invoicing-payer-detail' pk=payer.pk %}">
{{ payer.label }}
<span class="extra-info"> [{% trans "identifier:" %} {{ payer.slug }}]</span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<div class="big-msg-info">
{% blocktrans %}
This site doesn't have any payer yet. Click on the "New" button in the top
right of the page to add a first one.
{% endblocktrans %}
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends "lingo/invoicing/manager_payer_list.html" %}
{% load i18n gadjo %}
{% block breadcrumb %}
<a href="{% url 'lingo-manager-invoicing-payer-detail' payer.pk %}">{{ payer }}</a>
<a href="{% url 'lingo-manager-invoicing-payer-edit-mapping' payer.pk %}">{% trans "Edit mapping" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Edit mapping" %} - {{ payer }}</h2>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|with_template }}
<div class="buttons">
<button>{% trans "Submit" %}</button>
{% if object.pk %}
<a class="cancel" href="{% url 'lingo-manager-invoicing-payer-detail' payer.pk %}">{% trans 'Cancel' %}</a>
{% else %}
<a class="cancel" href="{% url 'lingo-manager-invoicing-payer-list' %}">{% trans 'Cancel' %}</a>
{% endif %}
</div>
</form>
{% endblock %}

View File

@ -40,6 +40,7 @@
<h3>{% trans "Parameters" %}</h3>
<ul>
<li>{% trans "Identifier:" %} {{ regie.slug }}</li>
<li>{% trans "Payer:" %} {% if regie.payer %}<a href="{% url 'lingo-manager-invoicing-payer-detail' pk=regie.payer.pk %}">{{ regie.payer|default:'' }}</a>{% endif %}</li>
<li>{% trans "Cashier role:" %} {{ regie.cashier_role|default:'' }}</li>
<li>{% trans "Counter name:" %} <code>{{ regie.counter_name }}</code></li>
<li>{% trans "Number format:" %} <code>{{ regie.number_format }}</code></li>

View File

@ -18,6 +18,7 @@ from django.urls import path
from .views import campaign as campaign_views
from .views import home as home_views
from .views import payer as payer_views
from .views import pool as pool_views
from .views import regie as regie_views
@ -146,4 +147,35 @@ urlpatterns = [
regie_views.regie_invoice_line_list,
name='lingo-manager-invoicing-regie-invoice-line-list',
),
path('payers/', payer_views.payers_list, name='lingo-manager-invoicing-payer-list'),
path(
'payer/add/',
payer_views.payer_add,
name='lingo-manager-invoicing-payer-add',
),
path(
'payer/<int:pk>/',
payer_views.payer_detail,
name='lingo-manager-invoicing-payer-detail',
),
path(
'payer/<int:pk>/edit/',
payer_views.payer_edit,
name='lingo-manager-invoicing-payer-edit',
),
path(
'payer/<int:pk>/mapping/',
payer_views.payer_edit_mapping,
name='lingo-manager-invoicing-payer-edit-mapping',
),
path(
'payer/<int:pk>/delete/',
payer_views.payer_delete,
name='lingo-manager-invoicing-payer-delete',
),
path(
'payer/<int:pk>/export/',
payer_views.payer_export,
name='lingo-manager-invoicing-payer-export',
),
]

View File

@ -23,7 +23,7 @@ from django.utils.translation import gettext_lazy as _
from lingo.agendas.chrono import get_check_status, get_subscriptions
from lingo.agendas.models import Agenda
from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, InjectedLine, Regie
from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, InjectedLine, Payer, PayerError, Regie
from lingo.pricing.models import AgendaPricing, AgendaPricingNotFound, PricingError
@ -76,7 +76,7 @@ def get_invoice_lines_for_user(
if payer_external_id not in payer_data_cache:
if agenda_pricing:
# will raise a PricingError if payer_data can not be computed
payer_data_cache[payer_external_id] = agenda_pricing.get_payer_data(
payer_data_cache[payer_external_id] = pool.campaign.regie.get_payer_data(
request, payer_external_id
)
elif payer_data:
@ -115,7 +115,7 @@ def get_invoice_lines_for_user(
payer_data = {}
try:
agenda_pricing = get_agenda_pricing(agendas_pricings_by_agendas.get(agenda.slug), event_date)
payer_external_id = agenda_pricing.get_payer_external_id(request, user_external_id)
payer_external_id = pool.campaign.regie.get_payer_external_id(request, user_external_id)
payer_data = get_cached_payer_data(request, payer_external_id, agenda_pricing=agenda_pricing)
pricing_data = agenda_pricing.get_pricing_data_for_event(
request=request,
@ -125,7 +125,7 @@ def get_invoice_lines_for_user(
user_external_id=user_external_id,
payer_external_id=payer_external_id,
)
except PricingError as e:
except (PayerError, PricingError) as e:
# AgendaPricingNotFound: can happen if pricing model defined only on a part of the requested period
pricing_error = {
'error': type(e).__name__,
@ -143,10 +143,10 @@ def get_invoice_lines_for_user(
user_first_name=user_first_name,
user_last_name=user_last_name,
payer_external_id=payer_external_id,
payer_first_name=payer_data.get('payer_first_name') or '',
payer_last_name=payer_data.get('payer_last_name') or '',
payer_demat=payer_data.get('payer_demat') or False,
payer_direct_debit=payer_data.get('payer_direct_debit') or False,
payer_first_name=payer_data.get('first_name') or '',
payer_last_name=payer_data.get('last_name') or '',
payer_demat=payer_data.get('demat') or False,
payer_direct_debit=payer_data.get('direct_debit') or False,
event=serialized_event,
pricing_data=pricing_error,
status='warning' if isinstance(e, AgendaPricingNotFound) else 'error',
@ -167,10 +167,10 @@ def get_invoice_lines_for_user(
user_first_name=user_first_name,
user_last_name=user_last_name,
payer_external_id=payer_external_id,
payer_first_name=payer_data['payer_first_name'],
payer_last_name=payer_data['payer_last_name'],
payer_demat=payer_data['payer_demat'],
payer_direct_debit=payer_data['payer_direct_debit'],
payer_first_name=payer_data['first_name'],
payer_last_name=payer_data['last_name'],
payer_demat=payer_data['demat'],
payer_direct_debit=payer_data['direct_debit'],
event=serialized_event,
pricing_data=pricing_data,
status='success',
@ -206,10 +206,10 @@ def get_invoice_lines_for_user(
for injected_line in injected_lines:
payer_external_id = injected_line.payer_external_id
payer_data = {
'payer_first_name': injected_line.payer_first_name,
'payer_last_name': injected_line.payer_last_name,
'payer_demat': injected_line.payer_demat,
'payer_direct_debit': injected_line.payer_direct_debit,
'first_name': injected_line.payer_first_name,
'last_name': injected_line.payer_last_name,
'demat': injected_line.payer_demat,
'direct_debit': injected_line.payer_direct_debit,
}
payer_data = get_cached_payer_data(request, payer_external_id, payer_data=payer_data)
lines.append(
@ -224,10 +224,10 @@ def get_invoice_lines_for_user(
user_first_name=user_first_name,
user_last_name=user_last_name,
payer_external_id=payer_external_id,
payer_first_name=payer_data['payer_first_name'],
payer_last_name=payer_data['payer_last_name'],
payer_demat=payer_data['payer_demat'],
payer_direct_debit=payer_data['payer_direct_debit'],
payer_first_name=payer_data['first_name'],
payer_last_name=payer_data['last_name'],
payer_demat=payer_data['demat'],
payer_direct_debit=payer_data['direct_debit'],
status='success',
pool=pool,
from_injected_line=injected_line,
@ -315,11 +315,14 @@ def generate_invoices_from_lines(all_lines, pool):
def export_site(
regies=True,
payers=True,
):
'''Dump site objects to JSON-dumpable dictionnary'''
data = {}
if regies:
data['regies'] = [x.export_json() for x in Regie.objects.all()]
if payers:
data['payers'] = [x.export_json() for x in Payer.objects.all()]
return data
@ -328,11 +331,12 @@ def import_site(data):
key: collections.defaultdict(list)
for key in [
'regies',
'payers',
]
}
with transaction.atomic():
for cls, key in ((Regie, 'regies'),):
for cls, key in ((Payer, 'payers'), (Regie, 'regies')):
objs = data.get(key, [])
for obj in objs:
created, obj = cls.import_json(obj)

View File

@ -90,6 +90,20 @@ class ConfigImportView(FormView):
x,
),
},
'payers': {
'create_noop': _('No payer created.'),
'create': lambda x: ngettext(
'A payer has been created.',
'%(count)d payers have been created.',
x,
),
'update_noop': _('No payer updated.'),
'update': lambda x: ngettext(
'A payer has been updated.',
'%(count)d payers have been updated.',
x,
),
},
}
global_noop = True
@ -113,8 +127,8 @@ class ConfigImportView(FormView):
else:
obj_results['messages'] = message2
(r_count,) = (len(results['regies']['all']),)
if (r_count,) == (1,):
(r_count, p_count) = (len(results['regies']['all']), len(results['payers']['all']))
if (r_count, p_count) == (1, 0):
# only one regie imported, redirect to regie page
return HttpResponseRedirect(
reverse(
@ -122,11 +136,20 @@ class ConfigImportView(FormView):
kwargs={'pk': results['regies']['all'][0].pk},
)
)
if (r_count, p_count) == (0, 1):
# only one payer imported, redirect to payer page
lguerin marked this conversation as resolved Outdated

only one payer je pense. (et redirect to payer page).

only one *payer* je pense. (et redirect to payer page).

oups, en effet

oups, en effet
return HttpResponseRedirect(
reverse(
'lingo-manager-invoicing-payer-detail',
kwargs={'pk': results['payers']['all'][0].pk},
)
)
if global_noop:
messages.info(self.request, _('No data found.'))
else:
messages.info(self.request, results['regies']['messages'])
messages.info(self.request, results['payers']['messages'])
return super().form_valid(form)

View File

@ -0,0 +1,113 @@
# 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 datetime
import json
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 NewPayerForm, PayerForm, PayerMappingForm
from lingo.invoicing.models import Payer
class PayersListView(ListView):
template_name = 'lingo/invoicing/manager_payer_list.html'
model = Payer
payers_list = PayersListView.as_view()
class PayerAddView(CreateView):
template_name = 'lingo/invoicing/manager_payer_form.html'
model = Payer
form_class = NewPayerForm
def get_success_url(self):
return reverse('lingo-manager-invoicing-payer-detail', args=[self.object.pk])
payer_add = PayerAddView.as_view()
class PayerDetailView(DetailView):
template_name = 'lingo/invoicing/manager_payer_detail.html'
model = Payer
def get_context_data(self, **kwargs):
kwargs['payer'] = self.object
kwargs['regies'] = self.object.regie_set.all()
return super().get_context_data(**kwargs)
payer_detail = PayerDetailView.as_view()
class PayerEditView(UpdateView):
template_name = 'lingo/invoicing/manager_payer_form.html'
model = Payer
form_class = PayerForm
def get_success_url(self):
return reverse('lingo-manager-invoicing-payer-detail', args=[self.object.pk])
payer_edit = PayerEditView.as_view()
class PayerEditMappingView(UpdateView):
template_name = 'lingo/invoicing/manager_payer_mapping_form.html'
model = Payer
form_class = PayerMappingForm
def get_success_url(self):
return '%s#open:mapping' % reverse('lingo-manager-invoicing-payer-detail', args=[self.object.pk])
payer_edit_mapping = PayerEditMappingView.as_view()
class PayerDeleteView(DeleteView):
template_name = 'lingo/manager_confirm_delete.html'
model = Payer
def get_queryset(self):
return super().get_queryset().filter(regie__isnull=True)
def get_success_url(self):
return reverse('lingo-manager-invoicing-payer-list')
payer_delete = PayerDeleteView.as_view()
class PayerExport(DetailView):
model = Payer
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
attachment = 'attachment; filename="export_payer_{}_{}.json"'.format(
self.get_object().slug, today.strftime('%Y%m%d')
)
response['Content-Disposition'] = attachment
json.dump({'payers': [self.get_object().export_json()]}, response, indent=2)
return response
payer_export = PayerExport.as_view()

View File

@ -95,16 +95,6 @@ class PricingVariableForm(forms.Form):
PricingVariableFormSet = forms.formset_factory(PricingVariableForm)
class PricingPayerForm(forms.Form):
key = forms.CharField(required=True, widget=forms.TextInput(attrs={'readonly': True}))
value = forms.CharField(
label=_('Value template'), widget=forms.TextInput(attrs={'size': 60}), required=False
)
PricingPayerFormSet = forms.formset_factory(PricingPayerForm, extra=0)
class PricingCriteriaCategoryAddForm(forms.Form):
category = forms.ModelChoiceField(
label=_('Criteria category to add'), queryset=CriteriaCategory.objects.none(), required=True

View File

@ -0,0 +1,14 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pricing', '0011_payer_variables'),
]
operations = [
migrations.RemoveField(
model_name='pricing',
name='payer_variables',
),
]

View File

@ -194,7 +194,6 @@ class Pricing(models.Model):
)
criterias = models.ManyToManyField(Criteria)
extra_variables = models.JSONField(blank=True, default=dict)
payer_variables = models.JSONField(blank=True, default=dict)
class Meta:
ordering = ['label']
@ -225,74 +224,6 @@ class Pricing(models.Model):
def get_extra_variables_keys(self):
return sorted((self.extra_variables or {}).keys())
def get_payer_variables_keys(self):
return [
'payer_external_id',
'payer_external_id_from_nameid',
'payer_first_name',
'payer_last_name',
'payer_demat',
'payer_direct_debit',
]
def get_payer_external_id(self, request, original_context, key='payer_external_id'):
context = RequestContext(request)
context.push(original_context)
tplt = self.payer_variables.get(key) or ''
if not tplt:
raise PayerError(details={'reason': 'empty-template'})
try:
value = Template(tplt).render(context)
if not value:
raise PayerError(details={'reason': 'empty-result'})
return value
except TemplateSyntaxError:
raise PayerError(details={'reason': 'syntax-error'})
except VariableDoesNotExist:
raise PayerError(details={'reason': 'variable-error'})
def get_payer_external_id_from_nameid(self, request, original_context):
return self.get_payer_external_id(request, original_context, key='payer_external_id_from_nameid')
def get_payer_data(self, request, original_context):
result = {}
context = RequestContext(request)
context.push(original_context)
bool_keys = ['payer_demat', 'payer_direct_debit']
for key in self.get_payer_variables_keys():
if key in ['payer_external_id', 'payer_external_id_from_nameid']:
continue
tplt = self.payer_variables.get(key) or ''
if not tplt:
if key not in bool_keys:
raise PayerDataError(
details={'key': key.removeprefix('payer_'), 'reason': 'empty-template'}
)
tplt = 'False'
try:
value = Template(tplt).render(context)
if not value:
if key not in bool_keys:
raise PayerDataError(
details={'key': key.removeprefix('payer_'), 'reason': 'empty-result'}
)
value = False
if key in bool_keys:
if value in ('True', 'true', '1'):
value = True
elif value in ('False', 'false', '0'):
value = False
else:
raise PayerDataError(
details={'key': key.removeprefix('payer_'), 'reason': 'not-a-boolean'}
)
result[key] = value
except TemplateSyntaxError:
raise PayerDataError(details={'key': key.removeprefix('payer_'), 'reason': 'syntax-error'})
except VariableDoesNotExist:
raise PayerDataError(details={'key': key.removeprefix('payer_'), 'reason': 'variable-error'})
return result
@classmethod
def import_json(cls, data):
data = data.copy()
@ -468,22 +399,6 @@ class AgendaPricing(models.Model):
return created, agenda_pricing
def get_payer_external_id(self, request, user_external_id):
context = {'user_external_id': user_external_id}
if ':' in user_external_id:
context['user_external_raw_id'] = user_external_id.split(':')[1]
return self.pricing.get_payer_external_id(request, context)
def get_payer_external_id_from_nameid(self, request, nameid):
context = {'nameid': nameid}
return self.pricing.get_payer_external_id_from_nameid(request, context)
def get_payer_data(self, request, payer_external_id):
context = {'payer_external_id': payer_external_id}
if ':' in payer_external_id:
context['payer_external_raw_id'] = payer_external_id.split(':')[1]
return self.pricing.get_payer_data(request, context)
def get_pricing_data(self, request, pricing_date, user_external_id, payer_external_id):
# compute pricing for flat_fee_schedule mode
data = {

View File

@ -27,7 +27,6 @@
<div class="pk-tabs">
<div class="pk-tabs--tab-list" role="tablist">
<button aria-controls="panel-variables" aria-selected="true" id="tab-variables" role="tab" tabindex="0">{% trans "Variables" %}</button>
<button aria-controls="panel-payer" aria-selected="false" id="tab-payer" role="tab" tabindex="-1">{% trans "Payer" %}</button>
<button aria-controls="panel-criterias" aria-selected="false" id="tab-criterias" role="tab" tabindex="-1">{% trans "Criterias" %}</button>
<button aria-controls="panel-usage" aria-selected="false" id="tab-usage" role="tab" tabindex="-1">{% trans "Used in agendas" %}</button>
</div>
@ -48,19 +47,6 @@
</div>
</div>
<div aria-labelledby="tab-payer" hidden="" id="panel-payer" role="tabpanel" tabindex="0">
<label>{% trans 'Payer variables:' %}</label>
<dl>
{% for key in object.get_payer_variables_keys %}
<dt><b>{% blocktrans %}{{ key }}:{% endblocktrans %}</b></dt>
<dd><pre>{{ object.payer_variables|get:key|default:'' }}</pre></dd>
{% endfor %}
</dl>
<div class="panel--buttons">
<a class="pk-button" rel="popup" href="{% url 'lingo-manager-pricing-payer-edit' pk=object.pk %}">{% trans 'Define payer variables' %}</a>
</div>
</div>
<div aria-labelledby="tab-criterias" hidden="" id="panel-criterias" role="tabpanel" tabindex="0">
{% with criterias=object.criterias.all categories=object.categories.all %}
{% if categories %}

View File

@ -1,38 +0,0 @@
{% extends "lingo/pricing/manager_pricing_detail.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-pricing-payer-edit' object.pk %}">{% trans "Payer variables definition" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Payer variables definition" %}</h2>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.management_form }}
<table>
<tbody>
{% for sub_form in form %}
<tr>
{% for field in sub_form %}
<td>
{{ field.errors.as_ul }}
{{ field }}
{% if forloop.first %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
<a class="cancel" href="{% url 'lingo-manager-pricing-detail' object.pk %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -58,11 +58,6 @@ urlpatterns = [
views.pricing_variable_edit,
name='lingo-manager-pricing-variable-edit',
),
path(
'model/<int:pk>/payer/',
views.pricing_payer_edit,
name='lingo-manager-pricing-payer-edit',
),
path(
'model/<int:pk>/category/add/',
views.pricing_criteria_category_add,

View File

@ -58,7 +58,6 @@ from lingo.pricing.forms import (
PricingCriteriaCategoryEditForm,
PricingDuplicateForm,
PricingMatrixForm,
PricingPayerFormSet,
PricingTestToolForm,
PricingVariableFormSet,
)
@ -411,43 +410,6 @@ class PricingVariableEdit(FormView):
pricing_variable_edit = PricingVariableEdit.as_view()
class PricingPayerEdit(FormView):
template_name = 'lingo/pricing/manager_pricing_payer_form.html'
model = Pricing
form_class = PricingPayerFormSet
def dispatch(self, request, *args, **kwargs):
self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['object'] = self.object
return super().get_context_data(**kwargs)
def get_initial(self):
return list(
{'key': k, 'value': self.object.payer_variables.get(k) or ''}
for k in self.object.get_payer_variables_keys()
)
def form_valid(self, form):
self.object.payer_variables = {k: '' for k in self.object.get_payer_variables_keys()}
for sub_data in form.cleaned_data:
if not sub_data.get('key'):
continue
if sub_data['key'] not in self.object.payer_variables:
continue
self.object.payer_variables[sub_data['key']] = sub_data['value']
self.object.save()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return '%s#open:payer' % reverse('lingo-manager-pricing-detail', args=[self.object.pk])
pricing_payer_edit = PricingPayerEdit.as_view()
class PricingCriteriaCategoryAddView(FormView):
template_name = 'lingo/pricing/manager_pricing_criteria_category_form.html'
model = Pricing

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

@ -7,7 +7,6 @@ from unittest import mock
import pytest
from django.utils.timezone import make_aware
from lingo.agendas.models import Agenda
from lingo.invoicing.models import (
Campaign,
DraftInvoice,
@ -16,11 +15,11 @@ from lingo.invoicing.models import (
Invoice,
InvoiceLine,
InvoicePayment,
PayerError,
Payment,
Pool,
Regie,
)
from lingo.pricing.models import AgendaPricing, Pricing
pytestmark = pytest.mark.django_db
@ -44,7 +43,7 @@ def test_regies(app, user):
]
@mock.patch.object(AgendaPricing, 'get_payer_external_id_from_nameid', autospec=True)
@mock.patch.object(Regie, 'get_payer_external_id_from_nameid', autospec=True)
def test_list_invoices(mock_payer, app, user):
app.get('/api/regie/foo/invoices/', status=403)
app.authorization = ('Basic', ('john.doe', 'password'))
@ -54,10 +53,6 @@ def test_list_invoices(mock_payer, app, user):
regie = Regie.objects.create(label='Foo')
app.get('/api/regie/foo/invoices/', status=404)
resp = app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
invoice = Invoice.objects.create(
label='My invoice',
date_publication=datetime.date(2022, 10, 1),
@ -66,48 +61,15 @@ def test_list_invoices(mock_payer, app, user):
regie=regie,
payer_external_id='payer:1',
)
InvoiceLine.objects.create(
event_date=datetime.date.today(),
invoice=invoice,
quantity=1,
unit_amount=42,
total_amount=42,
status='success',
)
invoice.refresh_from_db()
# no agenda pricing configured, no payer from name id
resp = app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
pricing = Pricing.objects.create(label='Model')
agenda1 = Agenda.objects.create(label='Foo Bar 1', regie=regie)
agenda2 = Agenda.objects.create(label='Foo Bar 2', regie=regie)
agenda3 = Agenda.objects.create(label='Foo Bar 3')
# agenda pricing configured, bot not for the regie
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda_pricing.agendas.add(agenda3)
# invoice remaining_amount is 0
assert invoice.remaining_amount == 0
mock_payer.return_value = 'payer:1'
resp = app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
assert mock_payer.call_args_list == []
# agenda pricing configured for the regie, but invoice remaining_amount is 0
InvoiceLine.objects.all().delete()
invoice.refresh_from_db()
assert invoice.remaining_amount == 0
agenda_pricing.agendas.add(agenda2)
resp = app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
assert mock_payer.call_args_list == [mock.call(agenda_pricing, mock.ANY, 'foobar')]
assert mock_payer.call_args_list == [mock.call(regie, mock.ANY, 'foobar')]
# invoice with something to pay
InvoiceLine.objects.create(
@ -191,46 +153,16 @@ def test_list_invoices(mock_payer, app, user):
assert resp.json['err'] == 0
assert resp.json['data'] == []
# more than one payer id
invoice_payment.amount = 2
invoice_payment.save()
mock_payer.reset_mock()
agenda_pricing2 = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda_pricing2.agendas.add(agenda1)
mock_payer.side_effect = ['payer:1', 'payer:other']
resp = app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == [
{
'amount': 40,
'amount_paid': 2,
'created': datetime.date.today().isoformat(),
'display_id': '',
'has_pdf': True,
'id': str(invoice.uuid),
'label': 'My invoice',
'online_payment': True,
'paid': False,
'pay_limit_date': datetime.date.today().isoformat(),
'total_amount': 42,
}
]
assert mock_payer.call_args_list == [
mock.call(agenda_pricing, mock.ANY, 'foobar'),
mock.call(agenda_pricing2, mock.ANY, 'foobar'),
]
# no matching payer id
mock_payer.side_effect = None
mock_payer.return_value = 'payer:unknown'
resp = app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
# payer error
mock_payer.side_effect = PayerError
app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'}, status=404)
def test_list_invoices_for_payer(app, user):
app.get('/api/regie/foo/invoices/', status=403)
@ -344,7 +276,7 @@ def test_list_invoices_for_payer(app, user):
assert resp.json['data'] == []
@mock.patch.object(AgendaPricing, 'get_payer_external_id_from_nameid', autospec=True)
@mock.patch.object(Regie, 'get_payer_external_id_from_nameid', autospec=True)
def test_list_history_invoices(mock_payer, app, user):
app.get('/api/regie/foo/invoices/history/', status=403)
app.authorization = ('Basic', ('john.doe', 'password'))
@ -354,10 +286,6 @@ def test_list_history_invoices(mock_payer, app, user):
regie = Regie.objects.create(label='Foo')
app.get('/api/regie/foo/invoices/history/', status=404)
resp = app.get('/api/regie/foo/invoices/history/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
invoice = Invoice.objects.create(
label='My invoice',
date_publication=datetime.date(2022, 10, 1),
@ -383,42 +311,17 @@ def test_list_history_invoices(mock_payer, app, user):
invoice_payment = InvoicePayment.objects.create(
payment=payment,
invoice=invoice,
amount=42,
amount=2,
)
invoice.refresh_from_db()
# no agenda pricing configured, no payer from name id
resp = app.get('/api/regie/foo/invoices/history/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
pricing = Pricing.objects.create(label='Model')
agenda1 = Agenda.objects.create(label='Foo Bar 1', regie=regie)
agenda2 = Agenda.objects.create(label='Foo Bar 2', regie=regie)
agenda3 = Agenda.objects.create(label='Foo Bar 3')
# agenda pricing configured, bot not for the regie
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda_pricing.agendas.add(agenda3)
# invoice remaining_amount is not 0
assert invoice.remaining_amount != 0
mock_payer.return_value = 'payer:1'
resp = app.get('/api/regie/foo/invoices/history/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
assert mock_payer.call_args_list == []
# agenda pricing configured for the regie, but invoice remaining_amount is not 0
invoice_payment.amount = 2
invoice_payment.save()
invoice.refresh_from_db()
assert invoice.remaining_amount != 0
agenda_pricing.agendas.add(agenda2)
resp = app.get('/api/regie/foo/invoices/history/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
assert mock_payer.call_args_list == [mock.call(agenda_pricing, mock.ANY, 'foobar')]
assert mock_payer.call_args_list == [mock.call(regie, mock.ANY, 'foobar')]
# invoice with nothing to pay
invoice_payment.amount = 42
@ -458,46 +361,16 @@ def test_list_history_invoices(mock_payer, app, user):
assert resp.json['err'] == 0
assert resp.json['data'] == []
# more than one payer id
invoice.regie = regie
invoice.save()
mock_payer.reset_mock()
agenda_pricing2 = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda_pricing2.agendas.add(agenda1)
mock_payer.side_effect = ['payer:1', 'payer:other']
resp = app.get('/api/regie/foo/invoices/history/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == [
{
'amount': 0,
'amount_paid': 42,
'created': datetime.date.today().isoformat(),
'display_id': '',
'has_pdf': True,
'id': str(invoice.uuid),
'label': 'My invoice',
'online_payment': False,
'paid': True,
'pay_limit_date': '',
'total_amount': 42,
}
]
assert mock_payer.call_args_list == [
mock.call(agenda_pricing, mock.ANY, 'foobar'),
mock.call(agenda_pricing2, mock.ANY, 'foobar'),
]
# no matching payer id
mock_payer.side_effect = None
mock_payer.return_value = 'payer:unknown'
resp = app.get('/api/regie/foo/invoices/history/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
# payer error
mock_payer.side_effect = PayerError
app.get('/api/regie/foo/invoices/history/', params={'NameID': 'foobar'}, status=404)
def test_list_history_invoices_for_payer(app, user):
app.get('/api/regie/foo/invoices/history/', status=403)
@ -585,7 +458,7 @@ def test_list_history_invoices_for_payer(app, user):
assert resp.json['data'] == []
@mock.patch.object(AgendaPricing, 'get_payer_external_id_from_nameid', autospec=True)
@mock.patch.object(Regie, 'get_payer_external_id_from_nameid', autospec=True)
def test_detail_invoice(mock_payer, app, user):
app.get('/api/regie/foo/invoice/%s/' % str(uuid.uuid4()), status=403)
app.authorization = ('Basic', ('john.doe', 'password'))
@ -615,27 +488,7 @@ def test_detail_invoice(mock_payer, app, user):
)
invoice.refresh_from_db()
# no agenda pricing configured, no payer from name id
resp = app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'}, status=404)
pricing = Pricing.objects.create(label='Model')
agenda1 = Agenda.objects.create(label='Foo Bar 1', regie=regie)
agenda2 = Agenda.objects.create(label='Foo Bar 2', regie=regie)
agenda3 = Agenda.objects.create(label='Foo Bar 3')
# agenda pricing configured, bot not for the regie
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda_pricing.agendas.add(agenda3)
mock_payer.return_value = 'payer:1'
resp = app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'}, status=404)
assert mock_payer.call_args_list == []
# agenda pricing configured for the regie
agenda_pricing.agendas.add(agenda2)
resp = app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == {
@ -712,24 +565,7 @@ def test_detail_invoice(mock_payer, app, user):
'total_amount': 42,
}
# more than one payer id
mock_payer.reset_mock()
agenda_pricing2 = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda_pricing2.agendas.add(agenda1)
mock_payer.side_effect = ['payer:1', 'payer:other']
resp = app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert mock_payer.call_args_list == [
mock.call(agenda_pricing, mock.ANY, 'foobar'),
mock.call(agenda_pricing2, mock.ANY, 'foobar'),
]
# no matching payer id
mock_payer.side_effect = None
mock_payer.return_value = 'payer:unknown'
resp = app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'}, status=404)
@ -846,7 +682,7 @@ def test_detail_invoice_for_payer(app, user):
}
@mock.patch.object(AgendaPricing, 'get_payer_external_id_from_nameid', autospec=True)
@mock.patch.object(Regie, 'get_payer_external_id_from_nameid', autospec=True)
def test_pdf_invoice(mock_payer, app, user):
app.get('/api/regie/foo/invoice/%s/pdf/' % str(uuid.uuid4()), status=403)
app.authorization = ('Basic', ('john.doe', 'password'))
@ -880,31 +716,7 @@ def test_pdf_invoice(mock_payer, app, user):
)
invoice.refresh_from_db()
# no agenda pricing configured, no payer from name id
resp = app.get(
'/api/regie/foo/invoice/%s/pdf/' % str(invoice.uuid), params={'NameID': 'foobar'}, status=404
)
pricing = Pricing.objects.create(label='Model')
agenda1 = Agenda.objects.create(label='Foo Bar 1', regie=regie)
agenda2 = Agenda.objects.create(label='Foo Bar 2', regie=regie)
agenda3 = Agenda.objects.create(label='Foo Bar 3')
# agenda pricing configured, bot not for the regie
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda_pricing.agendas.add(agenda3)
mock_payer.return_value = 'payer:1'
resp = app.get(
'/api/regie/foo/invoice/%s/pdf/' % str(invoice.uuid), params={'NameID': 'foobar'}, status=404
)
assert mock_payer.call_args_list == []
# agenda pricing configured for the regie
agenda_pricing.agendas.add(agenda2)
resp = app.get('/api/regie/foo/invoice/%s/pdf/' % str(invoice.uuid), params={'NameID': 'foobar'})
assert resp.headers['Content-Disposition'] == 'attachment; filename="%s.pdf"' % invoice.formatted_number
@ -924,29 +736,14 @@ def test_pdf_invoice(mock_payer, app, user):
'/api/regie/foo/invoice/%s/pdf/' % str(invoice.uuid), params={'NameID': 'foobar'}, status=404
)
# more than one payer id
invoice.regie = regie
invoice.save()
mock_payer.reset_mock()
agenda_pricing2 = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda_pricing2.agendas.add(agenda1)
mock_payer.side_effect = ['payer:1', 'payer:other']
resp = app.get('/api/regie/foo/invoice/%s/pdf/' % str(invoice.uuid), params={'NameID': 'foobar'})
assert resp.headers['Content-Disposition'] == 'attachment; filename="%s.pdf"' % invoice.formatted_number
assert mock_payer.call_args_list == [
mock.call(agenda_pricing, mock.ANY, 'foobar'),
mock.call(agenda_pricing2, mock.ANY, 'foobar'),
]
# no matching payer id
mock_payer.side_effect = None
mock_payer.return_value = 'payer:unknown'
resp = app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'}, status=404)
# payer error
mock_payer.side_effect = PayerError
app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'}, status=404)
def test_pdf_invoice_for_payer(app, user):
app.get('/api/regie/foo/invoice/%s/pdf/' % str(uuid.uuid4()), status=403)

View File

@ -22,3 +22,7 @@ def test_manager_invoicing_home(app, admin_user):
'div#lingo-manager-main div a[href="%s"]' % reverse('lingo-manager-invoicing-regie-list')
)
assert anchor.text().startswith('Regies')
anchor = resp.pyquery(
'div#lingo-manager-main div a[href="%s"]' % reverse('lingo-manager-invoicing-payer-list')
)
assert anchor.text().strip().startswith('Payers')

View File

@ -1528,8 +1528,10 @@ def test_detail_pool_invoices(app, admin_user, draft):
lines_resp = app.get(lines_url)
if draft:
assert len(lines_resp.pyquery('li')) == 3
assert len(lines_resp.pyquery('li[data-related-invoice-id="%s"]' % invoice1.pk)) == 3
else:
assert len(lines_resp.pyquery('li')) == 6
assert len(lines_resp.pyquery('li[data-related-invoice-id="%s"]' % invoice1.pk)) == 6
assert (
lines_resp.pyquery('li:nth-child(1)').text()
== '#%s User1 Name1 (user:1) - 01/09/2022 - Label 11 (1.00)' % line11.pk

View File

@ -4,7 +4,7 @@ import json
import pytest
from webtest import Upload
from lingo.invoicing.models import Regie
from lingo.invoicing.models import Payer, Regie
from tests.utils import login
pytestmark = pytest.mark.django_db
@ -25,6 +25,7 @@ def test_export_site(freezer, app, admin_user):
site_json = json.loads(resp.text)
assert site_json == {
'regies': [],
'payers': [],
}
Regie.objects.create(label='Foo Bar')
@ -34,6 +35,15 @@ def test_export_site(freezer, app, admin_user):
site_json = json.loads(resp.text)
assert len(site_json['regies']) == 1
resp = app.get('/manage/invoicing/export/')
resp.form['regies'] = False
resp.form['payers'] = False
resp = resp.form.submit()
site_json = json.loads(resp.text)
assert 'regies' not in site_json
assert 'payers' not in site_json
@pytest.mark.freeze_time('2023-06-02')
def test_import_regie(app, admin_user):
@ -93,3 +103,63 @@ def test_import_regie(app, admin_user):
resp = resp.form.submit().follow()
assert '3 regies have been created. No regie updated.' in resp.text
assert Regie.objects.count() == 3
@pytest.mark.freeze_time('2023-06-02')
def test_import_payer(app, admin_user):
payer = Payer.objects.create(label='Foo bar')
app = login(app)
resp = app.get('/manage/invoicing/payer/%s/' % payer.pk)
resp = resp.click(href='/manage/invoicing/payer/%s/export/' % payer.pk)
assert resp.headers['content-type'] == 'application/json'
assert resp.headers['content-disposition'] == 'attachment; filename="export_payer_foo-bar_20230602.json"'
payer_export = resp.text
# existing payer
resp = app.get('/manage/invoicing/', status=200)
resp = resp.click('Import')
resp.form['config_json'] = Upload('export.json', payer_export.encode('utf-8'), 'application/json')
resp = resp.form.submit()
assert resp.location.endswith('/manage/invoicing/payer/%s/' % payer.pk)
resp = resp.follow()
assert 'No payer created. A payer has been updated.' not in resp.text
assert Payer.objects.count() == 1
# new payer
Payer.objects.all().delete()
resp = app.get('/manage/invoicing/', status=200)
resp = resp.click('Import')
resp.form['config_json'] = Upload('export.json', payer_export.encode('utf-8'), 'application/json')
resp = resp.form.submit()
payer = Payer.objects.latest('pk')
assert resp.location.endswith('/manage/invoicing/payer/%s/' % payer.pk)
resp = resp.follow()
assert 'A payer has been created. No payer updated.' not in resp.text
assert Payer.objects.count() == 1
# multiple payers
payers = json.loads(payer_export)
payers['payers'].append(copy.copy(payers['payers'][0]))
payers['payers'].append(copy.copy(payers['payers'][0]))
payers['payers'][1]['label'] = 'Foo bar 2'
payers['payers'][1]['slug'] = 'foo-bar-2'
payers['payers'][2]['label'] = 'Foo bar 3'
payers['payers'][2]['slug'] = 'foo-bar-3'
resp = app.get('/manage/invoicing/', status=200)
resp = resp.click('Import')
resp.form['config_json'] = Upload('export.json', json.dumps(payers).encode('utf-8'), 'application/json')
resp = resp.form.submit()
assert resp.location.endswith('/manage/invoicing/')
resp = resp.follow()
assert '2 payers have been created. A payer has been updated.' in resp.text
assert Payer.objects.count() == 3
Payer.objects.all().delete()
resp = app.get('/manage/invoicing/', status=200)
resp = resp.click('Import')
resp.form['config_json'] = Upload('export.json', json.dumps(payers).encode('utf-8'), 'application/json')
resp = resp.form.submit().follow()
assert '3 payers have been created. No payer updated.' in resp.text
assert Payer.objects.count() == 3

View File

@ -0,0 +1,141 @@
from unittest import mock
import pytest
from lingo.invoicing.models import Payer, Regie
from tests.invoicing.utils import mocked_requests_send
from tests.utils import login
pytestmark = pytest.mark.django_db
@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'] = 'default:card_model_1'
resp.form['payer_external_id_prefix'] = 'payer:'
resp.form['payer_external_id_template'] = 'bar'
resp = resp.form.submit()
payer = Payer.objects.latest('pk')
assert resp.location.endswith('/manage/invoicing/payer/%s/' % payer.pk)
assert payer.label == 'Foo bar'
assert payer.slug == 'foo-bar'
assert payer.description == 'a little description'
assert payer.carddef_reference == 'default:card_model_1'
assert payer.payer_external_id_prefix == 'payer:'
assert payer.payer_external_id_template == 'bar'
@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:bar')
payer2 = Payer.objects.create(label='baz')
app = login(app)
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.']
resp.form['slug'] = 'baz2'
resp = resp.form.submit()
assert resp.location.endswith('/manage/invoicing/payer/%s/' % payer.pk)
payer.refresh_from_db()
assert payer.label == 'Foo bar baz'
assert payer.slug == 'baz2'
assert payer.carddef_reference == 'default:card_model_1'
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_edit_payer_mapping(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 '/manage/invoicing/payer/%s/mapping/' % payer.pk not in resp
payer.carddef_reference = 'default:card_model_1'
payer.save()
resp = app.get('/manage/invoicing/payer/%s/' % payer.pk)
resp = resp.click(href='/manage/invoicing/payer/%s/mapping/' % payer.pk)
assert len(resp.context['form'].fields) == 4
choices = [('', '-----'), ('fielda', 'Field A'), ('fieldb', 'Field B')]
assert resp.context['form'].fields['first_name'].choices == choices
assert resp.context['form'].fields['last_name'].choices == choices
assert resp.context['form'].fields['demat'].choices == choices
assert resp.context['form'].fields['direct_debit'].choices == choices
resp.form['first_name'] = 'fielda'
resp.form['last_name'] = 'fieldb'
resp = resp.form.submit()
assert resp.location.endswith('/manage/invoicing/payer/%s/#open:mapping' % payer.pk)
payer.refresh_from_db()
assert payer.user_fields_mapping == {
'first_name': 'fielda',
'last_name': 'fieldb',
'demat': '',
'direct_debit': '',
}
def test_delete_payer(app, admin_user):
payer = Payer.objects.create(label='Foo bar')
app = login(app)
resp = app.get('/manage/invoicing/payer/%s/' % payer.pk)
resp = resp.click(href='/manage/invoicing/payer/%s/delete/' % payer.pk)
resp = resp.form.submit()
assert resp.location.endswith('/manage/invoicing/payers/')
assert Payer.objects.exists() is False
# can not delete payer used in regie
payer.save()
Regie.objects.create(label='foo', payer=payer)
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:unknown'
payer.user_fields_mapping = {
'first_name': 'fielda',
'last_name': 'fieldb',
'demat': '',
'direct_debit': '',
}
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
assert resp.pyquery('dl dt').text() == 'First name: Last name: Demat: Direct debit:'
assert resp.pyquery('dl dd').text() == 'Field A Field B '

View File

@ -16,6 +16,7 @@ from lingo.invoicing.models import (
Invoice,
InvoiceLine,
InvoicePayment,
Payer,
Payment,
Pool,
Regie,
@ -94,7 +95,9 @@ def test_manager_invoicing_regie_detail(app, admin_user):
assert descr.text == 'foo description'
slug = resp.pyquery('div#panel-settings ul li')[0]
assert slug.text == 'Identifier: foo'
cashier_role = resp.pyquery('div#panel-settings ul li')[1]
slug = resp.pyquery('div#panel-settings ul li')[1]
assert slug.text == 'Payer: '
cashier_role = resp.pyquery('div#panel-settings ul li')[2]
assert cashier_role.text == 'Cashier role: role-foo'
usage = resp.pyquery('div#panel-usage div')[0]
assert 'This Regie is not used yet.' in usage.text
@ -172,6 +175,15 @@ def test_manager_invoicing_regie_edit(app, admin_user):
response = form.submit()
assert response.context['form'].errors['slug'] == ['Another regie exists with the same identifier.']
payer = Payer.objects.create(label='Foo Bar')
resp = app.get(reverse('lingo-manager-invoicing-regie-edit', kwargs={'pk': regie.pk}))
form = resp.form
form.set('payer', payer.pk)
resp = resp.form.submit().follow()
regie.refresh_from_db()
assert regie.payer == payer
assert 'Payer: <a href="/manage/invoicing/payer/%s/">Foo Bar</a>' % payer.pk in resp
def test_manager_invoicing_regie_delete(app, admin_user):
app = login(app)

View File

@ -0,0 +1,118 @@
import copy
import pytest
from lingo.invoicing.models import Payer, Regie
from lingo.invoicing.utils import export_site, import_site
from lingo.utils.misc import LingoImportError
pytestmark = pytest.mark.django_db
def test_import_export(app):
Regie.objects.create(label='Foo Bar')
Payer.objects.create(label='Foo Bar')
data = export_site()
assert len(data['regies']) == 1
assert len(data['payers']) == 1
import_site(data={})
assert Regie.objects.count() == 1
assert Payer.objects.count() == 1
def test_import_export_regies(app):
payload = export_site()
assert len(payload['regies']) == 0
regie = Regie.objects.create(label='Foo bar')
payload = export_site()
assert len(payload['regies']) == 1
regie.delete()
assert not Regie.objects.exists()
import_site(copy.deepcopy(payload))
assert Regie.objects.count() == 1
regie = Regie.objects.first()
assert regie.label == 'Foo bar'
assert regie.slug == 'foo-bar'
# update
update_payload = copy.deepcopy(payload)
update_payload['regies'][0]['label'] = 'Foo bar Updated'
import_site(update_payload)
regie.refresh_from_db()
assert regie.label == 'Foo bar Updated'
# insert another regie
regie.slug = 'foo-bar-updated'
regie.save()
import_site(copy.deepcopy(payload))
assert Regie.objects.count() == 2
regie = Regie.objects.latest('pk')
assert regie.label == 'Foo bar'
assert regie.slug == 'foo-bar'
def test_import_export_regie_with_payer(app):
payer = Payer.objects.create(label='Foo')
regie = Regie.objects.create(label='Bar', payer=payer)
data = export_site()
regie.payer = None
regie.save()
payer.delete()
del data['payers']
with pytest.raises(LingoImportError) as excinfo:
import_site(data)
assert str(excinfo.value) == 'Missing "foo" payer'
Payer.objects.create(label='Foobar')
with pytest.raises(LingoImportError) as excinfo:
import_site(data)
assert str(excinfo.value) == 'Missing "foo" payer'
payer = Payer.objects.create(label='Foo')
import_site(data)
regie.refresh_from_db()
assert regie.payer == payer
def test_import_export_payers(app):
payload = export_site()
assert len(payload['payers']) == 0
payer = Payer.objects.create(label='Foo bar', user_fields_mapping={'foo': 'bar'})
payload = export_site()
assert len(payload['payers']) == 1
payer.delete()
assert not Payer.objects.exists()
import_site(copy.deepcopy(payload))
assert Payer.objects.count() == 1
payer = Payer.objects.first()
assert payer.label == 'Foo bar'
assert payer.slug == 'foo-bar'
assert payer.user_fields_mapping == {'foo': 'bar'}
# update
update_payload = copy.deepcopy(payload)
update_payload['payers'][0]['label'] = 'Foo bar Updated'
import_site(update_payload)
payer.refresh_from_db()
assert payer.label == 'Foo bar Updated'
# insert another payer
payer.slug = 'foo-bar-updated'
payer.save()
import_site(copy.deepcopy(payload))
assert Payer.objects.count() == 2
payer = Payer.objects.latest('pk')
assert payer.label == 'Foo bar'
assert payer.slug == 'foo-bar'
assert payer.user_fields_mapping == {'foo': 'bar'}

View File

@ -18,6 +18,7 @@ from lingo.invoicing.models import (
InjectedLine,
Invoice,
InvoiceLine,
PayerError,
Pool,
PoolPromotionError,
Regie,
@ -27,7 +28,6 @@ from lingo.pricing.models import (
Criteria,
CriteriaCategory,
PayerDataError,
PayerError,
Pricing,
PricingError,
)
@ -253,7 +253,7 @@ def test_get_invoice_lines_for_user_check_status_error(mock_status):
@pytest.mark.parametrize('injected_lines', ['no', 'period', 'all'])
@mock.patch('lingo.invoicing.utils.get_check_status')
@mock.patch('lingo.pricing.models.AgendaPricing.get_payer_external_id')
@mock.patch('lingo.invoicing.models.Regie.get_payer_external_id')
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event')
def test_get_invoice_lines_for_user_check_status(
mock_pricing_data_event, mock_payer, mock_status, injected_lines
@ -526,10 +526,10 @@ def test_get_invoice_lines_for_user_check_status(
pool=pool,
payer_data_cache={
'payer:1': {
'payer_first_name': 'First1',
'payer_last_name': 'Last1',
'payer_demat': True,
'payer_direct_debit': False,
'first_name': 'First1',
'last_name': 'Last1',
'demat': True,
'direct_debit': False,
}
},
)
@ -768,7 +768,7 @@ def test_get_invoice_lines_for_user_check_status(
@mock.patch('lingo.invoicing.utils.get_check_status')
@mock.patch('lingo.pricing.models.AgendaPricing.get_payer_external_id')
@mock.patch('lingo.invoicing.models.Regie.get_payer_external_id')
def test_get_invoice_lines_for_user_get_payer_id_error(mock_payer, mock_status):
regie = Regie.objects.create(label='Regie')
agenda = Agenda.objects.create(label='Agenda')
@ -843,8 +843,8 @@ def test_get_invoice_lines_for_user_get_payer_id_error(mock_payer, mock_status):
@mock.patch('lingo.invoicing.utils.get_check_status')
@mock.patch('lingo.pricing.models.AgendaPricing.get_payer_external_id')
@mock.patch('lingo.pricing.models.AgendaPricing.get_payer_data')
@mock.patch('lingo.invoicing.models.Regie.get_payer_external_id')
@mock.patch('lingo.invoicing.models.Regie.get_payer_data')
def test_get_invoice_lines_for_user_get_payer_data_error(mock_payer_data, mock_payer, mock_status):
regie = Regie.objects.create(label='Regie')
agenda = Agenda.objects.create(label='Agenda')
@ -971,21 +971,21 @@ def test_get_invoice_lines_for_user_get_payer_id_and_data(mock_status):
def get_payer_data(ap, r, payer_external_id):
return {
'payer:1': {
'payer_first_name': 'First1',
'payer_last_name': 'Last1',
'payer_demat': True,
'payer_direct_debit': False,
'first_name': 'First1',
'last_name': 'Last1',
'demat': True,
'direct_debit': False,
},
'payer:2': {
'payer_first_name': 'First2',
'payer_last_name': 'Last2',
'payer_demat': False,
'payer_direct_debit': True,
'first_name': 'First2',
'last_name': 'Last2',
'demat': False,
'direct_debit': True,
},
}.get(payer_external_id)
payer_patch = mock.patch.object(AgendaPricing, 'get_payer_external_id', autospec=True)
payer_data_patch = mock.patch.object(AgendaPricing, 'get_payer_data', autospec=True)
payer_patch = mock.patch.object(Regie, 'get_payer_external_id', autospec=True)
payer_data_patch = mock.patch.object(Regie, 'get_payer_data', autospec=True)
with payer_patch as mock_payer, payer_data_patch as mock_payer_data:
mock_payer.side_effect = lambda *args: get_payer(*args)
mock_payer_data.side_effect = lambda *args: get_payer_data(*args)
@ -1033,26 +1033,26 @@ def test_get_invoice_lines_for_user_get_payer_id_and_data(mock_status):
# cache is populated
assert payer_data_cache == {
'payer:1': {
'payer_first_name': 'First1',
'payer_last_name': 'Last1',
'payer_demat': True,
'payer_direct_debit': False,
'first_name': 'First1',
'last_name': 'Last1',
'demat': True,
'direct_debit': False,
},
'payer:2': {
'payer_first_name': 'First2',
'payer_last_name': 'Last2',
'payer_demat': False,
'payer_direct_debit': True,
'first_name': 'First2',
'last_name': 'Last2',
'demat': False,
'direct_debit': True,
},
}
assert mock_payer.call_args_list == [
mock.call(agenda_pricing, mock.ANY, 'user:1'),
mock.call(agenda_pricing, mock.ANY, 'user:2'),
mock.call(agenda_pricing, mock.ANY, 'user:1'),
mock.call(regie, mock.ANY, 'user:1'),
mock.call(regie, mock.ANY, 'user:2'),
mock.call(regie, mock.ANY, 'user:1'),
]
assert mock_payer_data.call_args_list == [
mock.call(agenda_pricing, mock.ANY, 'payer:1'),
mock.call(agenda_pricing, mock.ANY, 'payer:2'),
mock.call(regie, mock.ANY, 'payer:1'),
mock.call(regie, mock.ANY, 'payer:2'),
# only 2 calls, payer:1 is cached after first call
]
@ -1113,20 +1113,20 @@ def test_get_invoice_lines_for_user_get_payer_id_and_data(mock_status):
# cache is populated
assert payer_data_cache == {
'payer:1': {
'payer_first_name': 'First1',
'payer_last_name': 'Last1',
'payer_demat': True,
'payer_direct_debit': False,
'first_name': 'First1',
'last_name': 'Last1',
'demat': True,
'direct_debit': False,
},
}
# but take in cache if present
payer_data_cache = {
'payer:1': {
'payer_first_name': 'First1 IN CACHE',
'payer_last_name': 'Last1 IN CACHE',
'payer_demat': False,
'payer_direct_debit': True,
'first_name': 'First1 IN CACHE',
'last_name': 'Last1 IN CACHE',
'demat': False,
'direct_debit': True,
},
}
@ -1152,7 +1152,7 @@ def test_get_invoice_lines_for_user_get_payer_id_and_data(mock_status):
@mock.patch('lingo.invoicing.utils.get_check_status')
@mock.patch('lingo.pricing.models.AgendaPricing.get_payer_external_id')
@mock.patch('lingo.invoicing.models.Regie.get_payer_external_id')
def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_payer, mock_status):
regie = Regie.objects.create(label='Regie')
agenda = Agenda.objects.create(label='Agenda')
@ -1218,10 +1218,10 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_payer
pool=pool,
payer_data_cache={
'payer:1': {
'payer_first_name': 'First1',
'payer_last_name': 'Last1',
'payer_demat': True,
'payer_direct_debit': False,
'first_name': 'First1',
'last_name': 'Last1',
'demat': True,
'direct_debit': False,
}
},
)
@ -1253,10 +1253,10 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_payer
pool=pool,
payer_data_cache={
'payer:1': {
'payer_first_name': 'First1',
'payer_last_name': 'Last1',
'payer_demat': True,
'payer_direct_debit': False,
'first_name': 'First1',
'last_name': 'Last1',
'demat': True,
'direct_debit': False,
}
},
)
@ -1287,10 +1287,10 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_payer
pool=pool,
payer_data_cache={
'payer:1': {
'payer_first_name': 'First1',
'payer_last_name': 'Last1',
'payer_demat': True,
'payer_direct_debit': False,
'first_name': 'First1',
'last_name': 'Last1',
'demat': True,
'direct_debit': False,
}
},
)
@ -1324,7 +1324,7 @@ def test_get_invoice_lines_for_user_check_status_agenda_pricing_dates(mock_payer
@mock.patch('lingo.invoicing.utils.get_check_status')
@mock.patch('lingo.pricing.models.AgendaPricing.get_payer_external_id')
@mock.patch('lingo.invoicing.models.Regie.get_payer_external_id')
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event')
def test_get_invoice_lines_for_user_check_status_pricing_error(
mock_pricing_data_event, mock_payer, mock_status
@ -1399,10 +1399,10 @@ def test_get_invoice_lines_for_user_check_status_pricing_error(
pool=pool,
payer_data_cache={
'payer:1': {
'payer_first_name': 'First1',
'payer_last_name': 'Last1',
'payer_demat': True,
'payer_direct_debit': False,
'first_name': 'First1',
'last_name': 'Last1',
'demat': True,
'direct_debit': False,
}
},
)
@ -1569,8 +1569,8 @@ def test_get_all_invoice_lines(mock_user_lines):
@mock.patch('lingo.invoicing.utils.get_check_status')
@mock.patch('lingo.pricing.models.AgendaPricing.get_payer_external_id')
@mock.patch('lingo.pricing.models.AgendaPricing.get_payer_data')
@mock.patch('lingo.invoicing.models.Regie.get_payer_external_id')
@mock.patch('lingo.invoicing.models.Regie.get_payer_data')
def test_get_all_invoice_lines_queryset(mock_payer_data, mock_payer, mock_status):
# don't mock get_pricing_data_for_event to check all querysets
category1 = CriteriaCategory.objects.create(label='Foo1', slug='foo1')
@ -1675,10 +1675,10 @@ def test_get_all_invoice_lines_queryset(mock_payer_data, mock_payer, mock_status
]
mock_payer.return_value = 'payer:1'
mock_payer_data.return_value = {
'payer_first_name': 'First1',
'payer_last_name': 'Last1',
'payer_demat': True,
'payer_direct_debit': False,
'first_name': 'First1',
'last_name': 'Last1',
'demat': True,
'direct_debit': False,
}
with CaptureQueriesContext(connection) as ctx:

View File

@ -1,8 +1,12 @@
import datetime
from unittest import mock
import pytest
from django.db import transaction
from django.db.utils import InternalError
from django.template import Context
from django.test.client import RequestFactory
from publik_django_templatetags.wcs.context_processors import Cards
from lingo.invoicing.models import (
Campaign,
@ -12,14 +16,28 @@ from lingo.invoicing.models import (
Invoice,
InvoiceLine,
InvoicePayment,
Payer,
PayerDataError,
PayerError,
Payment,
Pool,
Regie,
)
from tests.invoicing.utils import mocked_requests_send
pytestmark = pytest.mark.django_db
@pytest.fixture
def context():
return Context(
{
'cards': Cards(),
'request': RequestFactory().get('/'),
}
)
@pytest.mark.parametrize('draft', [True, False])
@pytest.mark.parametrize('orphan', [True, False])
def test_invoice_total_amount(draft, orphan):
@ -416,3 +434,160 @@ def test_regie_format_number():
regie.save()
assert regie.format_number(datetime.date(2023, 2, 15), 42) == 'Ffoobar-2023-00000042'
assert regie.format_number(datetime.date(2024, 12, 15), 42000000) == 'Ffoobar-2024-42000000'
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_get_payer_external_id(mock_send, context, nocache):
payer = Payer.objects.create(label='Payer')
regie = Regie.objects.create(label='Regie')
with pytest.raises(PayerError) as e:
regie.get_payer_external_id(request=context['request'], user_external_id='child:42')
assert e.value.details == {'reason': 'missing-payer'}
regie.payer = payer
regie.save()
values = [
('bar', 'bar'),
('{{ 40|add:2 }}', '42'),
('{{ cards|objects:"card_model_1"|first|get:"id" }}', '42'),
]
for value, result in values:
payer.payer_external_id_prefix = ''
payer.payer_external_id_template = value
payer.save()
assert regie.get_payer_external_id(request=context['request'], user_external_id='child:42') == result
payer.payer_external_id_prefix = 'prefix:'
payer.save()
assert (
regie.get_payer_external_id(request=context['request'], user_external_id='child:42')
== 'prefix:%s' % result
)
values = [
('', 'empty-template'),
('{{ "" }}', 'empty-result'),
('{% for %}', 'syntax-error'),
('{{ "foo"|add:user.email }}', 'variable-error'),
]
for value, error in values:
payer.payer_external_id_template = value
payer.save()
with pytest.raises(PayerError) as e:
regie.get_payer_external_id(request=context['request'], user_external_id='child:42')
assert e.value.details == {'reason': error}
# user_external_id can be used in variables
payer.payer_external_id_template = (
'{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_id|first|get:"id" }}'
)
payer.save()
mock_send.reset_mock()
regie.get_payer_external_id(request=context['request'], user_external_id='child:42')
assert 'filter-foo=child%3A42&' in mock_send.call_args_list[0][0][0].url
payer.payer_external_id_template = (
'{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_raw_id|first|get:"id" }}',
)
payer.save()
mock_send.reset_mock()
regie.get_payer_external_id(request=context['request'], user_external_id='child:42')
assert 'filter-foo=42&' in mock_send.call_args_list[0][0][0].url
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_get_payer_external_id_from_nameid(mock_send, context, nocache):
payer = Payer.objects.create(label='Payer')
regie = Regie.objects.create(label='Regie')
with pytest.raises(PayerError) as e:
regie.get_payer_external_id_from_nameid(request=context['request'], nameid='foobar')
assert e.value.details == {'reason': 'missing-payer'}
regie.payer = payer
regie.save()
with pytest.raises(PayerError) as e:
regie.get_payer_external_id_from_nameid(request=context['request'], nameid='foobar')
assert e.value.details == {'reason': 'missing-card-model'}
payer.carddef_reference = 'default:card_model_1'
payer.save()
regie.get_payer_external_id_from_nameid(request=context['request'], nameid='foobar')
assert '/api/cards/card_model_1/list?' in mock_send.call_args_list[-1][0][0].url
assert '&filter-user-uuid=foobar&' in mock_send.call_args_list[-1][0][0].url
assert regie.get_payer_external_id_from_nameid(request=context['request'], nameid='foobar') == '42'
payer.payer_external_id_prefix = 'prefix:'
payer.save()
assert regie.get_payer_external_id_from_nameid(request=context['request'], nameid='foobar') == 'prefix:42'
payer.carddef_reference = 'default:card_model_2'
payer.save()
with pytest.raises(PayerError) as e:
regie.get_payer_external_id_from_nameid(request=context['request'], nameid='foobar')
assert e.value.details == {'reason': 'empty-result'}
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_get_payer_data(mock_send, context, nocache):
payer = Payer.objects.create(label='Payer')
regie = Regie.objects.create(label='Regie')
with pytest.raises(PayerError) as e:
regie.get_payer_data(request=context['request'], payer_external_id='payer:42')
assert e.value.details == {'reason': 'missing-payer'}
regie.payer = payer
regie.save()
with pytest.raises(PayerError) as e:
regie.get_payer_data(request=context['request'], payer_external_id='payer:42')
assert e.value.details == {'reason': 'missing-card-model'}
payer.carddef_reference = 'default:card_model_1'
payer.save()
original_variables = {
'first_name': 'fielda',
'last_name': 'fielda',
'demat': 'fieldb',
'direct_debit': 'fieldb',
}
payer_data = {
'first_name': 'foo',
'last_name': 'foo',
'demat': True,
'direct_debit': True,
}
payer.user_fields_mapping = original_variables.copy()
payer.save()
assert regie.get_payer_data(request=context['request'], payer_external_id='payer:42') == payer_data
assert '/api/cards/card_model_1/list?' in mock_send.call_args_list[-1][0][0].url
assert (
'&filter-internal-id=42&filter-internal-id-operator=eq&include-fields=on'
in mock_send.call_args_list[-1][0][0].url
)
for key in ['first_name', 'last_name']:
payer.user_fields_mapping = original_variables.copy()
payer.user_fields_mapping[key] = ''
payer.save()
with pytest.raises(PayerDataError) as e:
regie.get_payer_data(request=context['request'], payer_external_id='payer:42')
assert e.value.details == {'key': key, 'reason': 'not-defined'}
for key in ['demat', 'direct_debit']:
payer.user_fields_mapping = original_variables.copy()
payer.user_fields_mapping[key] = ''
payer.save()
data_result = payer_data.copy()
data_result[key] = False
assert regie.get_payer_data(request=context['request'], payer_external_id='payer:42') == data_result
payer.user_fields_mapping = original_variables.copy()
payer.user_fields_mapping[key] = 'fielda'
payer.save()
with pytest.raises(PayerDataError) as e:
regie.get_payer_data(request=context['request'], payer_external_id='payer:42')
assert e.value.details == {'key': key, 'reason': 'not-a-boolean'}

66
tests/invoicing/utils.py Normal file
View File

@ -0,0 +1,66 @@
import copy
import json
import re
import urllib.parse
from unittest import mock
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'},
]
WCS_CARDDEF_SCHEMAS = {
'card_model_1': {
'name': 'Card Model 1',
'fields': [
{'label': 'Field A', 'varname': 'fielda', 'type': 'string'},
{'label': 'Field B', 'varname': 'fieldb', 'type': 'bool'},
],
}
}
WCS_CARDS_DATA = {
'card_model_1': [
{
'id': 42,
'display_id': '10-42',
'display_name': 'Card Model 1 - n°10-42',
'digest': 'a a a',
'text': 'aa',
'fields': {
'fielda': 'foo',
'fieldb': True,
},
},
]
}
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
m_schema = re.match(r'/api/cards/([a-z0-9_]+)/@schema', url)
if m_schema:
return WCS_CARDDEF_SCHEMAS.get(m_schema.group(1)) or {}
m_list = re.match(r'/api/cards/([a-z0-9_]+)/list', url)
if m_list:
return WCS_CARDS_DATA.get(m_list.group(1)) or []
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}))

View File

@ -167,47 +167,6 @@ def test_pricing_edit_extra_variables(app, admin_user):
assert '<dd><pre>bar</pre></dd>' in resp
def test_pricing_edit_payer_variables(app, admin_user):
pricing = Pricing.objects.create(label='Model')
assert pricing.payer_variables == {}
app = login(app)
resp = app.get('/manage/pricing/model/%s/' % pricing.pk)
assert '<label>Payer variables:</label>' in resp.text
assert '<dt><b>payer_external_id:</b></dt>' in resp
assert '<dt><b>payer_external_id_from_nameid:</b></dt>' in resp
assert '<dt><b>payer_first_name:</b></dt>' in resp
assert '<dt><b>payer_last_name:</b></dt>' in resp
assert '<dt><b>payer_demat:</b></dt>' in resp
assert '<dt><b>payer_direct_debit:</b></dt>' in resp
assert resp.text.count('<dd><pre></pre></dd>') == 6
resp = resp.click(href='/manage/pricing/model/%s/payer/' % pricing.pk)
assert resp.form['form-TOTAL_FORMS'].value == '6'
assert resp.form['form-0-key'].value == 'payer_external_id'
assert resp.form['form-0-value'].value == ''
assert resp.form['form-1-key'].value == 'payer_external_id_from_nameid'
assert resp.form['form-1-value'].value == ''
assert resp.form['form-2-key'].value == 'payer_first_name'
assert resp.form['form-2-value'].value == ''
assert resp.form['form-3-key'].value == 'payer_last_name'
assert resp.form['form-3-value'].value == ''
assert resp.form['form-4-key'].value == 'payer_demat'
assert resp.form['form-4-value'].value == ''
assert resp.form['form-5-key'].value == 'payer_direct_debit'
assert resp.form['form-5-value'].value == ''
resp.form['form-0-value'] = 'payer:42'
resp = resp.form.submit().follow()
pricing.refresh_from_db()
assert pricing.payer_variables == {
'payer_external_id': 'payer:42',
'payer_external_id_from_nameid': '',
'payer_first_name': '',
'payer_last_name': '',
'payer_demat': '',
'payer_direct_debit': '',
}
def test_pricing_add_category(app, admin_user):
pricing = Pricing.objects.create(label='Model')
category1 = CriteriaCategory.objects.create(label='Cat 1')

View File

@ -16,8 +16,6 @@ from lingo.pricing.models import (
CriteriaCategory,
CriteriaConditionNotFound,
MultipleDefaultCriteriaCondition,
PayerDataError,
PayerError,
Pricing,
PricingBookingCheckTypeError,
PricingBookingNotCheckedError,
@ -1688,218 +1686,3 @@ def test_agenda_pricing_iter_pricing_matrix_empty():
agenda_pricing.agendas.add(agenda)
assert list(agenda_pricing.iter_pricing_matrix()) == []
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_get_payer_external_id(mock_send, context, nocache):
pricing = Pricing.objects.create(label='Foo bar')
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
values = [
('bar', 'bar'),
('{{ 40|add:2 }}', '42'),
('{{ cards|objects:"foo"|first|get:"id" }}', '1'),
]
for value, result in values:
pricing.payer_variables = {
'payer_external_id': value,
}
pricing.save()
assert (
agenda_pricing.get_payer_external_id(request=context['request'], user_external_id='child:42')
== result
)
values = [
('', 'empty-template'),
('{{ "" }}', 'empty-result'),
('{% for %}', 'syntax-error'),
('{{ "foo"|add:user.email }}', 'variable-error'),
]
for value, error in values:
pricing.payer_variables = {
'payer_external_id': value,
}
pricing.save()
with pytest.raises(PayerError) as e:
agenda_pricing.get_payer_external_id(request=context['request'], user_external_id='child:42')
assert e.value.details == {'reason': error}
# user_external_id can be used in variables
pricing.payer_variables = {
'payer_external_id': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_id|first|get:"id" }}',
}
pricing.save()
mock_send.reset_mock()
agenda_pricing.get_payer_external_id(request=context['request'], user_external_id='child:42')
assert 'filter-foo=child%3A42&' in mock_send.call_args_list[0][0][0].url
pricing.payer_variables = {
'payer_external_id': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_raw_id|first|get:"id" }}',
}
pricing.save()
mock_send.reset_mock()
agenda_pricing.get_payer_external_id(request=context['request'], user_external_id='child:42')
assert 'filter-foo=42&' in mock_send.call_args_list[0][0][0].url
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_get_payer_external_id_from_nameid(mock_send, context, nocache):
pricing = Pricing.objects.create(label='Foo bar')
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
values = [
('bar', 'bar'),
('{{ 40|add:2 }}', '42'),
('{{ cards|objects:"foo"|first|get:"id" }}', '1'),
]
for value, result in values:
pricing.payer_variables = {
'payer_external_id_from_nameid': value,
}
pricing.save()
assert (
agenda_pricing.get_payer_external_id_from_nameid(request=context['request'], nameid='foobar')
== result
)
values = [
('', 'empty-template'),
('{{ "" }}', 'empty-result'),
('{% for %}', 'syntax-error'),
('{{ "foo"|add:user.email }}', 'variable-error'),
]
for value, error in values:
pricing.payer_variables = {
'payer_external_id_from_nameid': value,
}
pricing.save()
with pytest.raises(PayerError) as e:
agenda_pricing.get_payer_external_id_from_nameid(request=context['request'], nameid='foobar')
assert e.value.details == {'reason': error}
pricing.payer_variables = {
'payer_external_id_from_nameid': '{{ cards|objects:"qf"|filter_by_user:nameid|first|get:"id" }}',
}
pricing.save()
mock_send.reset_mock()
agenda_pricing.get_payer_external_id_from_nameid(request=context['request'], nameid='foobar')
assert 'filter-user-uuid=foobar&' in mock_send.call_args_list[0][0][0].url
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
def test_get_payer_data(mock_send, context, nocache):
pricing = Pricing.objects.create(label='Foo bar')
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
original_variables = {
'payer_first_name': 'First',
'payer_last_name': 'Last',
'payer_demat': 'False',
'payer_direct_debit': 'True',
}
payer_data = {
'payer_first_name': 'First',
'payer_last_name': 'Last',
'payer_demat': False,
'payer_direct_debit': True,
}
for key in ['payer_first_name', 'payer_last_name']:
values = [
('bar', 'bar'),
('{{ 40|add:2 }}', '42'),
('{{ cards|objects:"foo"|first|get:"id" }}', '1'),
]
for value, result in values:
pricing.payer_variables = original_variables.copy()
pricing.payer_variables[key] = value
pricing.save()
data_result = payer_data.copy()
data_result[key] = result
assert (
agenda_pricing.get_payer_data(request=context['request'], payer_external_id='payer:42')
== data_result
)
values = [
('', 'empty-template'),
('{{ "" }}', 'empty-result'),
('{% for %}', 'syntax-error'),
('{{ "foo"|add:user.email }}', 'variable-error'),
]
for value, error in values:
pricing.payer_variables = original_variables.copy()
pricing.payer_variables[key] = value
pricing.save()
with pytest.raises(PayerDataError) as e:
agenda_pricing.get_payer_data(request=context['request'], payer_external_id='payer:42')
assert e.value.details == {'key': key.removeprefix('payer_'), 'reason': error}
for key in ['payer_demat', 'payer_direct_debit']:
values = [
('', False),
('True', True),
('true', True),
('1', True),
('False', False),
('false', False),
('0', False),
('{{ cards|objects:"foo"|first|get:"fields"|get:"bar" }}', False),
('{{ cards|objects:"foo"|last|get:"fields"|get:"bar" }}', True),
]
for value, result in values:
pricing.payer_variables = original_variables.copy()
pricing.payer_variables[key] = value
pricing.save()
data_result = payer_data.copy()
data_result[key] = result
assert (
agenda_pricing.get_payer_data(request=context['request'], payer_external_id='payer:42')
== data_result
)
values = [
('{% for %}', 'syntax-error'),
('{{ "foo"|add:user.email }}', 'variable-error'),
('{{ cards|objects:"foo"|last|get:"fields"|get:"foo" }}', 'not-a-boolean'),
]
for value, error in values:
pricing.payer_variables = original_variables.copy()
pricing.payer_variables[key] = value
pricing.save()
with pytest.raises(PayerDataError) as e:
agenda_pricing.get_payer_data(request=context['request'], payer_external_id='payer:42')
assert e.value.details == {'key': key.removeprefix('payer_'), 'reason': error}
# payer_external_id can be used in variables
pricing.payer_variables = original_variables.copy()
pricing.payer_variables.update(
{
'payer_first_name': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:payer_external_id|first|get:"id" }}',
}
)
pricing.save()
mock_send.reset_mock()
agenda_pricing.get_payer_data(request=context['request'], payer_external_id='child:42')
assert 'filter-foo=child%3A42&' in mock_send.call_args_list[0][0][0].url
pricing.payer_variables.update(
{
'payer_first_name': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:payer_external_raw_id|first|get:"id" }}',
}
)
pricing.save()
mock_send.reset_mock()
agenda_pricing.get_payer_data(request=context['request'], payer_external_id='child:42')
assert 'filter-foo=42&' in mock_send.call_args_list[0][0][0].url