Modèle de payeur (#78015) #60
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']},
|
||||
),
|
||||
]
|
|
@ -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'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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 = {
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 '
|
|
@ -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)
|
||||
|
|
|
@ -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'}
|
|
@ -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:
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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}))
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
only one payer je pense. (et redirect to payer page).
oups, en effet