Facturation - trouver le payeur (sans garde alternée) (#74498) #31

Merged
lguerin merged 8 commits from wip/74498-invoicing-payer-id into main 2023-03-16 10:55:43 +01:00
21 changed files with 1664 additions and 223 deletions

View File

@ -43,7 +43,7 @@ class PricingComputeSerializer(serializers.Serializer):
agenda_pricing = serializers.SlugField(required=False, allow_blank=False, max_length=160)
start_date = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
user_external_id = serializers.CharField(required=True, max_length=250)
adult_external_id = serializers.CharField(required=True, max_length=250)
payer_external_id = serializers.CharField(required=True, max_length=250)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -192,7 +192,7 @@ class PricingComputeSerializer(serializers.Serializer):
'check_type': None,
},
user_external_id=self.validated_data['user_external_id'],
adult_external_id=self.validated_data['adult_external_id'],
payer_external_id=self.validated_data['payer_external_id'],
)
result.append(
{
@ -227,7 +227,7 @@ class PricingComputeSerializer(serializers.Serializer):
self._billing_date.date_start if self._billing_date else self._agenda_pricing.date_start
),
user_external_id=self.validated_data['user_external_id'],
adult_external_id=self.validated_data['adult_external_id'],
payer_external_id=self.validated_data['payer_external_id'],
)
result['pricing_data'] = pricing_data
return result

View File

@ -60,7 +60,6 @@ class AbstractInvoiceFilterSet(django_filters.FilterSet):
)
payer_external_id = django_filters.CharFilter(
label=_('Payer (external ID)'),
field_name='payer',
)
user_external_id = django_filters.CharFilter(
label=_('User (external ID)'),

View File

@ -0,0 +1,99 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0017_campaign_regie'),
]
operations = [
migrations.RenameField(
model_name='draftinvoiceline',
old_name='user_name',
new_name='user_last_name',
),
migrations.RenameField(
model_name='invoiceline',
old_name='user_name',
new_name='user_last_name',
),
migrations.AddField(
model_name='draftinvoiceline',
name='payer_demat',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='draftinvoiceline',
name='payer_direct_debit',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='draftinvoiceline',
name='payer_first_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='draftinvoiceline',
name='payer_last_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='draftinvoiceline',
name='user_first_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='injectedline',
name='payer_demat',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='injectedline',
name='payer_direct_debit',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='injectedline',
name='payer_first_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='injectedline',
name='payer_last_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceline',
name='payer_demat',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='invoiceline',
name='payer_direct_debit',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='invoiceline',
name='payer_first_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceline',
name='payer_last_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='invoiceline',
name='user_first_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
]

View File

@ -0,0 +1,75 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0018_payer'),
]
operations = [
migrations.RenameField(
model_name='draftinvoice',
old_name='payer',
new_name='payer_external_id',
),
migrations.RenameField(
model_name='invoice',
old_name='payer',
new_name='payer_external_id',
),
migrations.AlterField(
model_name='draftinvoice',
name='payer_external_id',
field=models.CharField(max_length=250),
),
migrations.AlterField(
model_name='invoice',
name='payer_external_id',
field=models.CharField(max_length=250),
),
migrations.AddField(
model_name='draftinvoice',
name='payer_demat',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='draftinvoice',
name='payer_direct_debit',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='draftinvoice',
name='payer_first_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='draftinvoice',
name='payer_last_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='invoice',
name='payer_demat',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='invoice',
name='payer_direct_debit',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='invoice',
name='payer_first_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
migrations.AddField(
model_name='invoice',
name='payer_last_name',
field=models.CharField(default='', max_length=250),
preserve_default=False,
),
]

View File

@ -291,7 +291,11 @@ class AbstractInvoice(models.Model):
total_amount = models.DecimalField(max_digits=9, decimal_places=2, default=0)
date_issue = models.DateField(_('Issue date'))
regie = models.ForeignKey(Regie, on_delete=models.PROTECT)
payer = models.CharField(_('Payer'), max_length=300)
payer_external_id = models.CharField(max_length=250)
payer_first_name = models.CharField(max_length=250)
payer_last_name = models.CharField(max_length=250)
payer_demat = models.BooleanField(default=False)
payer_direct_debit = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
pool = models.ForeignKey(Pool, on_delete=models.PROTECT)
@ -344,6 +348,10 @@ class InjectedLine(models.Model):
user_external_id = models.CharField(max_length=250)
payer_external_id = models.CharField(max_length=250)
payer_first_name = models.CharField(max_length=250)
payer_last_name = models.CharField(max_length=250)
payer_demat = models.BooleanField(default=False)
payer_direct_debit = models.BooleanField(default=False)
regie = models.ForeignKey(Regie, on_delete=models.PROTECT)
@ -356,8 +364,13 @@ class AbstractInvoiceLine(models.Model):
total_amount = models.DecimalField(max_digits=9, decimal_places=2)
user_external_id = models.CharField(max_length=250)
user_name = models.CharField(max_length=250)
user_first_name = models.CharField(max_length=250)
user_last_name = models.CharField(max_length=250)
payer_external_id = models.CharField(max_length=250)
payer_first_name = models.CharField(max_length=250)
payer_last_name = models.CharField(max_length=250)
payer_demat = models.BooleanField(default=False)
payer_direct_debit = models.BooleanField(default=False)
event = JSONField(default=dict)
pricing_data = JSONField(default=dict, encoder=DjangoJSONEncoder)
status = models.CharField(
@ -374,6 +387,11 @@ class AbstractInvoiceLine(models.Model):
class Meta:
abstract = True
@property
def user_name(self):
user_name = '%s %s' % (self.user_first_name, self.user_last_name)
return user_name.strip()
def get_error_display(self):
if self.status == 'success':
return
@ -391,12 +409,14 @@ class AbstractInvoiceLine(models.Model):
'PricingEventNotCheckedError': _('Event is not checked'),
'PricingBookingNotCheckedError': _('Booking is not checked'),
'PricingMultipleBookingError': _('Multiple booking found'),
'PricingBookingCheckTypeError': _('Check type error: %(checktype_error)s'),
'PricingBookingCheckTypeError': _('Check type error: %(reason)s'),
'PayerError': _('Impossible to determine payer: %(reason)s'),
'PayerDataError': _('Impossible to get payer %(key)s: %(reason)s'),
}
formats = {
'decimal': _('decimal'),
}
checktype_error = None
reason = None
if error_details.get('reason'):
reasons = {
'not-found': _('not found'),
@ -404,8 +424,13 @@ class AbstractInvoiceLine(models.Model):
'not-configured': _(
'pricing not configured (group: %(check_type_group)s, check type: %(check_type)s)'
),
'empty-template': _('Template is empty'),
'empty-result': _('Result is empty'),
'syntax-error': _('Syntax error'),
'variable-error': _('Variable error'),
'not-a-boolean': _('Result is not a boolean'),
}
checktype_error = reasons.get(error_details['reason']) % {
reason = reasons.get(error_details['reason']) % {
'check_type': error_details.get('check_type'),
'check_type_group': error_details.get('check_type_group'),
}
@ -421,7 +446,8 @@ class AbstractInvoiceLine(models.Model):
'pricing': error_details.get('pricing'),
'wanted': formats.get(error_details.get('wanted')),
'status': error_details.get('status'),
'checktype_error': checktype_error,
'reason': reason,
'key': error_details.get('key'),
}
or error_class
)

View File

@ -51,9 +51,9 @@
{% for invoice in object_list %}
<li class="invoice untoggled" data-invoice-id="{{ invoice.pk }}" data-invoice-lines-url="{% url 'lingo-manager-invoicing-invoice-line-list' regie_pk=regie.pk pk=object.pk pool_pk=pool.pk invoice_pk=invoice.pk %}">
{% if pool.draft %}
{% blocktrans with number=invoice.pk payer=invoice.payer amount=invoice.total_amount %}Invoice <a href="{{ journal_url }}?invoice_id={{ number }}">PROFORMA-{{ number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer }}">{{ payer }}</a>, amount {{ amount }}€{% endblocktrans %}
{% blocktrans with number=invoice.pk payer=invoice.payer_external_id amount=invoice.total_amount %}Invoice <a href="{{ journal_url }}?invoice_id={{ number }}">PROFORMA-{{ number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer }}">{{ payer }}</a>, amount {{ amount }}€{% endblocktrans %}
{% else %}
{% blocktrans with invoice_number=invoice.formatted_number payer=invoice.payer amount=invoice.total_amount number=invoice.number %}Invoice <a href="{{ journal_url }}?invoice_number={{ number }}">{{ invoice_number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer }}">{{ payer }}</a>, amount {{ amount }}€{% endblocktrans %}
{% blocktrans with invoice_number=invoice.formatted_number payer=invoice.payer_external_id amount=invoice.total_amount number=invoice.number %}Invoice <a href="{{ journal_url }}?invoice_number={{ number }}">{{ invoice_number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer }}">{{ payer }}</a>, amount {{ amount }}€{% endblocktrans %}
{% endif %}
<span class="togglable"></span>
</li>

View File

@ -48,12 +48,19 @@ def get_users_from_subscriptions(agendas, pool):
user_external_id = subscription['user_external_id']
if user_external_id in users:
continue
user_name = '%s %s' % (subscription['user_first_name'], subscription['user_last_name'])
users[user_external_id] = user_name.strip()[:250] or user_external_id
return [(user_id, user_name) for user_id, user_name in users.items()]
users[user_external_id] = (subscription['user_first_name'], subscription['user_last_name'])
return list(users.items())
def get_invoice_lines_for_user(agendas, agendas_pricings, user_external_id, user_name, pool):
def get_invoice_lines_for_user(
agendas,
agendas_pricings,
user_external_id,
user_first_name,
user_last_name,
pool,
payer_data_cache,
):
def get_agenda_pricing(agendas_pricings_for_agenda, date_event):
# same logic as AgendaPricing.get_agenda_pricing
for agenda_pricing in agendas_pricings_for_agenda:
@ -64,6 +71,17 @@ def get_invoice_lines_for_user(agendas, agendas_pricings, user_external_id, user
return agenda_pricing
raise AgendaPricingNotFound
def get_cached_payer_data(request, payer_external_id, agenda_pricing=None, payer_data=None):
Review

Ok, juste une question de forme ici j’aurais bien vu un def get_cached_payer_data comme nom de fonction, pour que quelqu’un qui (comme moi) ne connaît pas du tout le code comprenne plus directement, à la lecture du code qui l’utilise plus loin, l’appel à première vue un peu mystérieux :

payer_data = get_payer_data(…, payer_data=payer_data)
Ok, juste une question de forme ici j’aurais bien vu un `def get_cached_payer_data` comme nom de fonction, pour que quelqu’un qui (comme moi) ne connaît pas du tout le code comprenne plus directement, à la lecture du code qui l’utilise plus loin, l’appel à première vue un peu mystérieux : <pre> payer_data = get_payer_data(…, payer_data=payer_data) </pre>
Review

fait, merci

fait, merci
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(
request, payer_external_id
)
elif payer_data:
payer_data_cache[payer_external_id] = payer_data
return payer_data_cache.get(payer_external_id) or {}
if not agendas:
return []
@ -92,15 +110,19 @@ def get_invoice_lines_for_user(agendas, agendas_pricings, user_external_id, user
event_slug = '%s@%s' % (serialized_event['agenda'], serialized_event['slug'])
agenda = agendas_by_slug[serialized_event['agenda']]
payer_external_id = _('unknown')
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_data = get_cached_payer_data(request, payer_external_id, agenda_pricing=agenda_pricing)
pricing_data = agenda_pricing.get_pricing_data_for_event(
request=request,
agenda=agenda,
event=serialized_event,
check_status=check_status['check_status'],
user_external_id=user_external_id,
adult_external_id=user_external_id, # XXX
payer_external_id=payer_external_id,
)
except PricingError as e:
# AgendaPricingNotFound: can happen if pricing model defined only on a part of the requested period
@ -117,8 +139,13 @@ def get_invoice_lines_for_user(agendas, agendas_pricings, user_external_id, user
unit_amount=0,
total_amount=0,
user_external_id=user_external_id,
user_name=user_name,
payer_external_id=user_external_id, # XXX
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,
event=serialized_event,
pricing_data=pricing_error,
status='warning' if isinstance(e, AgendaPricingNotFound) else 'error',
@ -136,8 +163,13 @@ def get_invoice_lines_for_user(agendas, agendas_pricings, user_external_id, user
unit_amount=pricing_data['pricing'],
total_amount=pricing_data['pricing'],
user_external_id=user_external_id,
user_name=user_name,
payer_external_id=user_external_id, # XXX
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'],
event=serialized_event,
pricing_data=pricing_data,
status='success',
@ -171,6 +203,14 @@ def get_invoice_lines_for_user(agendas, agendas_pricings, user_external_id, 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,
}
payer_data = get_cached_payer_data(request, payer_external_id, payer_data=payer_data)
lines.append(
DraftInvoiceLine(
event_date=injected_line.event_date,
@ -180,8 +220,13 @@ def get_invoice_lines_for_user(agendas, agendas_pricings, user_external_id, user
unit_amount=injected_line.unit_amount,
total_amount=injected_line.total_amount,
user_external_id=user_external_id,
user_name=user_name,
payer_external_id=injected_line.user_external_id,
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'],
status='success',
pool=pool,
from_injected_line=injected_line,
@ -204,14 +249,17 @@ def get_all_invoice_lines(agendas, users, pool):
)
lines = []
for user_external_id, user_name in users:
payer_data_cache = {}
for user_external_id, (user_first_name, user_last_name) in users:
# generate lines for each user
lines += get_invoice_lines_for_user(
agendas=agendas,
agendas_pricings=agendas_pricings,
user_external_id=user_external_id,
user_name=user_name,
user_first_name=user_first_name,
user_last_name=user_last_name,
pool=pool,
payer_data_cache=payer_data_cache,
)
return lines
@ -226,12 +274,18 @@ def generate_invoices_from_lines(all_lines, pool):
# ignore lines in error
continue
if line.payer_external_id not in lines:
lines[line.payer_external_id] = []
lines[line.payer_external_id].append(line)
lines[line.payer_external_id] = {
'payer_first_name': line.payer_first_name,
'payer_last_name': line.payer_last_name,
'payer_demat': line.payer_demat,
'payer_direct_debit': line.payer_direct_debit,
'lines': [],
}
lines[line.payer_external_id]['lines'].append(line)
# generate invoices by regie and by payer_external_id (payer)
invoices = []
for payer_external_id, adult_lines in lines.items():
for payer_external_id, payer_data in lines.items():
invoice = DraftInvoice.objects.create(
label=_('Invoice from %(start)s to %(end)s')
% {
@ -240,10 +294,16 @@ def generate_invoices_from_lines(all_lines, pool):
},
date_issue=pool.campaign.date_issue,
regie=regie,
payer=payer_external_id,
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'],
pool=pool,
)
DraftInvoiceLine.objects.filter(pk__in=[line.pk for line in adult_lines]).update(invoice=invoice)
DraftInvoiceLine.objects.filter(pk__in=[line.pk for line in payer_data['lines']]).update(
invoice=invoice
)
invoices.append(invoice)
return invoices

View File

@ -635,7 +635,8 @@ class NonInvoicedLineListView(ListView):
'total_amount',
'user_external_id',
'payer_external_id',
'user_name',
'user_first_name',
'user_last_name',
'event',
'pricing_data',
'status',
@ -647,7 +648,8 @@ class NonInvoicedLineListView(ListView):
qs2 = (
InjectedLine.objects.filter(invoiceline__isnull=True, regie=self.regie)
.annotate(
user_name=Value('', output_field=CharField()),
user_first_name=Value('', output_field=CharField()),
user_last_name=Value('', output_field=CharField()),
event=Value({}, output_field=JSONField()),
pricing_data=Value({}, output_field=JSONField()),
status=Value('injected', output_field=CharField()),
@ -664,6 +666,9 @@ class NonInvoicedLineListView(ListView):
pools = Pool.objects.filter(draft=False).in_bulk()
for line in context['object_list']:
if line['status'] == 'error':
line['user_name'] = InvoiceLine(
user_first_name=line['user_first_name'], user_last_name=line['user_last_name']
).user_name
line['error_display'] = InvoiceLine(
status=line['status'], pricing_data=line['pricing_data']
).get_error_display()

View File

@ -95,6 +95,16 @@ 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
@ -296,7 +306,7 @@ class PricingTestToolForm(forms.Form):
label=_('Billing date'), empty_label=None, queryset=BillingDate.objects.none()
)
user_external_id = forms.CharField(label=_('User external identifier'))
adult_external_id = forms.CharField(label=_('Adult external identifier'))
payer_external_id = forms.CharField(label=_('Payer external identifier'))
booking_status = forms.ChoiceField(label=_('Booking status'), choices=[])
def __init__(self, *args, **kwargs):
@ -409,7 +419,7 @@ class PricingTestToolForm(forms.Form):
request=self.request,
pricing_date=pricing_date,
user_external_id=self.cleaned_data['user_external_id'],
adult_external_id=self.cleaned_data['adult_external_id'],
payer_external_id=self.cleaned_data['payer_external_id'],
)
def compute_for_event(self):
@ -422,7 +432,7 @@ class PricingTestToolForm(forms.Form):
'check_type': self.check_type_slug,
},
user_external_id=self.cleaned_data['user_external_id'],
adult_external_id=self.cleaned_data['adult_external_id'],
payer_external_id=self.cleaned_data['payer_external_id'],
)

View File

@ -0,0 +1,17 @@
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pricing', '0010_flat_fee_schedule'),
]
operations = [
migrations.AddField(
model_name='pricing',
name='payer_variables',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
),
]

View File

@ -40,6 +40,14 @@ class AgendaPricingNotFound(PricingError):
pass
class PayerError(PricingError):
pass
class PayerDataError(PricingError):
pass
class CriteriaConditionNotFound(PricingError):
pass
@ -191,6 +199,7 @@ class Pricing(models.Model):
)
criterias = models.ManyToManyField(Criteria)
extra_variables = JSONField(blank=True, default=dict)
payer_variables = JSONField(blank=True, default=dict)
class Meta:
ordering = ['label']
@ -221,6 +230,70 @@ 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_first_name',
'payer_last_name',
'payer_demat',
'payer_direct_debit',
]
def get_payer_external_id(self, request, original_context):
context = RequestContext(request)
context.push(original_context)
tplt = self.payer_variables.get('payer_external_id') 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_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 == 'payer_external_id':
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:
Review

Je ne comprends pas ici en quoi le fait d’avoir une clé associée à une valeur autre que booléenne fait qu’on interdit un résultat vide :/

Je ne comprends pas ici en quoi le fait d’avoir une clé associée à une valeur autre que booléenne fait qu’on interdit un résultat vide :/
Review

Les champs payer de type bool (demat, direct_debit) ont une valeur par défaut, "False".
Les autres champs n'ont pas de valeur par défaut (nom, prénom, impossible de définir une valeur par défaut), donc on ne veut pas de résultat vide.
C'est un peu arbitraire, je me suis dit qu'on pouvait ne pas définir de template pour les champs bool et ça passe quand même, alors que pour les autres champs c'est plus embêtant pour la suite (édition d'une facture etc) d'avoir une valeur vide.
Peut-être que je me suis embêtée pour rien et qu'on pourrait péter une erreur pour les bools aussi.
On verra à l'usage :)

Les champs payer de type bool (demat, direct_debit) ont une valeur par défaut, "False". Les autres champs n'ont pas de valeur par défaut (nom, prénom, impossible de définir une valeur par défaut), donc on ne veut pas de résultat vide. C'est un peu arbitraire, je me suis dit qu'on pouvait ne pas définir de template pour les champs bool et ça passe quand même, alors que pour les autres champs c'est plus embêtant pour la suite (édition d'une facture etc) d'avoir une valeur vide. Peut-être que je me suis embêtée pour rien et qu'on pourrait péter une erreur pour les bools aussi. On verra à l'usage :)
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, overwrite=False):
data = data.copy()
@ -400,7 +473,19 @@ class AgendaPricing(models.Model):
return created, agenda_pricing
def get_pricing_data(self, request, pricing_date, user_external_id, adult_external_id):
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_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 = {
'pricing_date': pricing_date, # date to use for QF
@ -409,7 +494,7 @@ class AgendaPricing(models.Model):
request=request,
data=data,
user_external_id=user_external_id,
adult_external_id=adult_external_id,
payer_external_id=payer_external_id,
)
pricing, criterias = self.compute_pricing(context=context)
return {
@ -422,7 +507,7 @@ class AgendaPricing(models.Model):
}
def get_pricing_data_for_event(
self, request, agenda, event, check_status, user_external_id, adult_external_id
self, request, agenda, event, check_status, user_external_id, payer_external_id
):
# compute pricing for an event
event_date = datetime.datetime.fromisoformat(event['start_datetime']).date()
@ -434,7 +519,7 @@ class AgendaPricing(models.Model):
request=request,
data=data,
user_external_id=user_external_id,
adult_external_id=adult_external_id,
payer_external_id=payer_external_id,
)
pricing, criterias = self.compute_pricing(context=context)
modifier = self.get_booking_modifier(agenda=agenda, check_status=check_status)
@ -468,12 +553,12 @@ class AgendaPricing(models.Model):
except (AgendaPricing.DoesNotExist, AgendaPricing.MultipleObjectsReturned):
raise AgendaPricingNotFound
def get_pricing_context(self, request, data, user_external_id, adult_external_id):
context = {'data': data, 'user_external_id': user_external_id, 'adult_external_id': adult_external_id}
def get_pricing_context(self, request, data, user_external_id, payer_external_id):
context = {'data': data, 'user_external_id': user_external_id, 'payer_external_id': payer_external_id}
if ':' in user_external_id:
context['user_external_raw_id'] = user_external_id.split(':')[1]
if ':' in adult_external_id:
context['adult_external_raw_id'] = adult_external_id.split(':')[1]
if ':' in payer_external_id:
context['payer_external_raw_id'] = payer_external_id.split(':')[1]
return self.pricing.get_extra_variables(request, context)
def format_pricing_data(self):

View File

@ -27,6 +27,7 @@
<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>
@ -35,13 +36,31 @@
<div aria-labelledby="tab-variables" id="panel-variables" role="tabpanel" tabindex="0">
{% if object.extra_variables %}
<label>{% trans 'Extra variables:' %}</label>
{% for key in object.get_extra_variables_keys %}<i>{{ key }}</i>{% if not forloop.last %}, {% endif %}{% endfor %}
<dl>
{% for key in object.get_extra_variables_keys %}
<dt><b>{% blocktrans %}{{ key }}:{% endblocktrans %}</b></dt>
<dd><pre>{{ object.extra_variables|get:key }}</pre></dd>
Review

Là juste question d’affichage, je me dis qu’avec <pre/> il peut y avoir des soucis d’affichage si la valeur est trop longue et dépasse la largeur allouée dans le conteneur. Je ne sais si ça peut arriver, mais j’imagine qu’on peut gérer cela par la suite, dans une autre PR, si ça survenait.

Là juste question d’affichage, je me dis qu’avec `<pre/>` il peut y avoir des soucis d’affichage si la valeur est trop longue et dépasse la largeur allouée dans le conteneur. Je ne sais si ça peut arriver, mais j’imagine qu’on peut gérer cela par la suite, dans une autre PR, si ça survenait.
Review

Effectivement ça dépasse facilement, en local j'ai un template minimal pour aller chercher un champ dans une fiche et ça dépasse de l'écran.
Mais j'ai voulu faire un premier truc rapide et pas forcément joli, pour avancer; on pourra faire un ticket joliesse pour améliorer tout ça.

Effectivement ça dépasse facilement, en local j'ai un template minimal pour aller chercher un champ dans une fiche et ça dépasse de l'écran. Mais j'ai voulu faire un premier truc rapide et pas forcément joli, pour avancer; on pourra faire un ticket joliesse pour améliorer tout ça.
{% endfor %}
</dl>
{% endif %}
<div class="panel--buttons">
<a class="pk-button" rel="popup" href="{% url 'lingo-manager-pricing-variable-edit' pk=object.pk %}">{% trans 'Define variables' %}</a>
</div>
</div>
<div aria-labelledby="tab-payer" hidden="" id="panel-payer" role="tabpanel" tabindex="0">
<label>{% trans 'Payer variables:' %}</label>
<dl>
{% for key in object.get_payer_variables_keys %}
<dt><b>{% blocktrans %}{{ key }}:{% endblocktrans %}</b></dt>
<dd><pre>{{ object.payer_variables|get:key|default:'' }}</pre></dd>
{% endfor %}
</dl>
<div class="panel--buttons">
<a class="pk-button" rel="popup" href="{% url 'lingo-manager-pricing-payer-edit' pk=object.pk %}">{% trans 'Define payer variables' %}</a>
</div>
</div>
<div aria-labelledby="tab-criterias" hidden="" id="panel-criterias" role="tabpanel" tabindex="0">
{% with criterias=object.criterias.all categories=object.categories.all %}
{% if categories %}

View File

@ -0,0 +1,38 @@
{% 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>
Review

Petite typo ici dans la chaîne internationalisée (ariables -> variables).

Petite typo ici dans la chaîne internationalisée (ariables -> variables).
Review

corrigé

corrigé
{% 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>
Review

J’avoue que là je n’ai pas essayé de checkout la branche pour voir l’affichage du formset en tableau avec un formulaire par ligne comme ça. Totale confiance en l’auteure de la PR là-dessus :)

J’avoue que là je n’ai pas essayé de checkout la branche pour voir l’affichage du formset en tableau avec un formulaire par ligne comme ça. Totale confiance en l’auteure de la PR là-dessus :)
Review

copier/coller d'un autre template déjà existant, et on doit avoir des trucs similaires dans combo et passerelle :)

copier/coller d'un autre template déjà existant, et on doit avoir des trucs similaires dans combo et passerelle :)
{% for sub_form in form %}
<tr>
{% for field in sub_form %}
<td>
{{ field.errors.as_ul }}
{{ field }}
{% if forloop.first %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
<a class="cancel" href="{% url 'lingo-manager-pricing-detail' object.pk %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

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

View File

@ -58,6 +58,7 @@ from lingo.pricing.forms import (
PricingCriteriaCategoryEditForm,
PricingDuplicateForm,
PricingMatrixForm,
PricingPayerFormSet,
PricingTestToolForm,
PricingVariableFormSet,
)
@ -410,6 +411,43 @@ 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

View File

@ -26,7 +26,7 @@ def test_pricing_compute_params(app, user):
# missing slots, agenda, agenda_pricing
resp = app.get(
'/api/pricing/compute/',
params={'user_external_id': 'user:1', 'adult_external_id': 'adult:1'},
params={'user_external_id': 'user:1', 'payer_external_id': 'payer:1'},
status=400,
)
assert resp.json['err'] == 1
@ -38,7 +38,7 @@ def test_pricing_compute_params(app, user):
# missing start_date
resp = app.get(
'/api/pricing/compute/',
params={'agenda': 'foo', 'user_external_id': 'user:1', 'adult_external_id': 'adult:1'},
params={'agenda': 'foo', 'user_external_id': 'user:1', 'payer_external_id': 'payer:1'},
status=400,
)
assert resp.json['err'] == 1
@ -46,7 +46,7 @@ def test_pricing_compute_params(app, user):
assert resp.json['errors']['start_date'] == ['This field is required when using "agenda" parameter.']
resp = app.get(
'/api/pricing/compute/',
params={'agenda_pricing': 'foo', 'user_external_id': 'user:1', 'adult_external_id': 'adult:1'},
params={'agenda_pricing': 'foo', 'user_external_id': 'user:1', 'payer_external_id': 'payer:1'},
status=400,
)
assert resp.json['err'] == 1
@ -63,7 +63,7 @@ def test_pricing_compute_params(app, user):
for param in params:
# missing user_external_id
_param = param.copy()
_param.update({'adult_external_id': 'adult:1'})
_param.update({'payer_external_id': 'payer:1'})
resp = app.get(
'/api/pricing/compute/',
params=_param,
@ -73,7 +73,7 @@ def test_pricing_compute_params(app, user):
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['user_external_id'] == ['This field is required.']
# missing adult_external_id
# missing payer_external_id
_param = param.copy()
_param.update({'user_external_id': 'user:1'})
resp = app.get(
@ -83,7 +83,7 @@ def test_pricing_compute_params(app, user):
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['adult_external_id'] == ['This field is required.']
assert resp.json['errors']['payer_external_id'] == ['This field is required.']
def test_pricing_compute_slots(app, user):
@ -95,7 +95,7 @@ def test_pricing_compute_slots(app, user):
params={
'slots': 'event-bar-slug',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
status=400,
)
@ -107,7 +107,7 @@ def test_pricing_compute_slots(app, user):
params={
'slots': '@event-bar-slug',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
status=400,
)
@ -119,7 +119,7 @@ def test_pricing_compute_slots(app, user):
params={
'slots': 'agenda@',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
status=400,
)
@ -133,7 +133,7 @@ def test_pricing_compute_slots(app, user):
params={
'slots': 'agenda@event-bar-slug, agenda2@event-bar-slug',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
status=400,
)
@ -152,7 +152,7 @@ def test_pricing_compute_agenda(app, user):
'agenda': 'agenda',
'start_date': '2021-09-01',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
status=400,
)
@ -171,7 +171,7 @@ def test_pricing_compute_agenda_pricing(app, user):
'agenda_pricing': 'baz',
'start_date': '2021-09-01',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
status=400,
)
@ -221,7 +221,7 @@ def test_pricing_compute_events_error(mock_events, app, user):
params={
'slots': 'agenda@event-bar-slug',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
status=400,
)
@ -257,7 +257,7 @@ def test_pricing_compute_for_event(mock_pricing_data_event, mock_events, app, us
params={
'slots': 'agenda@event-bar-slug, agenda2@recurring-event-baz-slug, agenda2@recurring-event-baz-slug:1',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == [
@ -284,7 +284,7 @@ def test_pricing_compute_for_event(mock_pricing_data_event, mock_events, app, us
params={
'slots': 'agenda@event-bar-slug, agenda2@recurring-event-baz-slug, agenda2@recurring-event-baz-slug:1',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == [
@ -306,7 +306,7 @@ def test_pricing_compute_for_event(mock_pricing_data_event, mock_events, app, us
params={
'slots': 'agenda@event-bar-slug, agenda2@recurring-event-baz-slug, agenda2@recurring-event-baz-slug:1',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == [
@ -325,7 +325,7 @@ def test_pricing_compute_for_event(mock_pricing_data_event, mock_events, app, us
},
check_status={'status': 'presence', 'check_type': None},
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
),
mock.call(
request=mock.ANY,
@ -337,7 +337,7 @@ def test_pricing_compute_for_event(mock_pricing_data_event, mock_events, app, us
},
check_status={'status': 'presence', 'check_type': None},
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
),
]
# with a start_date
@ -351,7 +351,7 @@ def test_pricing_compute_for_event(mock_pricing_data_event, mock_events, app, us
params={
'slots': 'agenda@event-bar-slug, agenda2@recurring-event-baz-slug, agenda2@recurring-event-baz-slug:1',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
'start_date': '2021-09-06',
},
)
@ -371,7 +371,7 @@ def test_pricing_compute_for_event(mock_pricing_data_event, mock_events, app, us
},
check_status={'status': 'presence', 'check_type': None},
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
),
mock.call(
request=mock.ANY,
@ -383,7 +383,7 @@ def test_pricing_compute_for_event(mock_pricing_data_event, mock_events, app, us
},
check_status={'status': 'presence', 'check_type': None},
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
),
]
@ -397,7 +397,7 @@ def test_pricing_compute_for_event(mock_pricing_data_event, mock_events, app, us
params={
'slots': 'agenda@event-bar-slug, agenda2@recurring-event-baz-slug, agenda2@recurring-event-baz-slug:1',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == [
@ -434,7 +434,7 @@ def test_pricing_compute_for_flat_fee_schedule_with_subscription(mock_pricing_da
'agenda': 'foo-bar',
'start_date': '2021-09-02',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == {
@ -457,7 +457,7 @@ def test_pricing_compute_for_flat_fee_schedule_with_subscription(mock_pricing_da
'agenda': 'foo-bar',
'start_date': '2021-09-02',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == {
@ -474,7 +474,7 @@ def test_pricing_compute_for_flat_fee_schedule_with_subscription(mock_pricing_da
'agenda': 'foo-bar',
'start_date': '2021-09-02',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == {
@ -491,7 +491,7 @@ def test_pricing_compute_for_flat_fee_schedule_with_subscription(mock_pricing_da
'agenda': 'foo-bar',
'start_date': '2021-09-02',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == {'agenda': 'foo-bar', 'pricing_data': {'foo': 'bar'}}
@ -500,7 +500,7 @@ def test_pricing_compute_for_flat_fee_schedule_with_subscription(mock_pricing_da
request=mock.ANY,
pricing_date=datetime.date(2021, 9, 1),
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
),
]
@ -512,7 +512,7 @@ def test_pricing_compute_for_flat_fee_schedule_with_subscription(mock_pricing_da
'agenda': 'foo-bar',
'start_date': '2021-09-02',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == {
@ -538,7 +538,7 @@ def test_pricing_compute_for_flat_fee_schedule_with_subscription(mock_pricing_da
'agenda': 'foo-bar',
'start_date': '2021-09-16',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert mock_pricing_data.call_args_list == [
@ -546,7 +546,7 @@ def test_pricing_compute_for_flat_fee_schedule_with_subscription(mock_pricing_da
request=mock.ANY,
pricing_date=datetime.date(2021, 9, 15),
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
),
]
@ -571,7 +571,7 @@ def test_pricing_compute_for_flat_fee_schedule_without_subscription(mock_pricing
'agenda_pricing': 'foo-bar-pricing',
'start_date': '2021-09-02',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == {'agenda_pricing': 'foo-bar-pricing', 'pricing_data': {'foo': 'bar'}}
@ -580,7 +580,7 @@ def test_pricing_compute_for_flat_fee_schedule_without_subscription(mock_pricing
request=mock.ANY,
pricing_date=datetime.date(2021, 9, 1),
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
),
]
@ -592,7 +592,7 @@ def test_pricing_compute_for_flat_fee_schedule_without_subscription(mock_pricing
'agenda_pricing': 'foo-bar-pricing',
'start_date': '2021-09-02',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert resp.json['data'] == {
@ -618,7 +618,7 @@ def test_pricing_compute_for_flat_fee_schedule_without_subscription(mock_pricing
'agenda_pricing': 'foo-bar-pricing',
'start_date': '2021-09-16',
'user_external_id': 'user:1',
'adult_external_id': 'adult:1',
'payer_external_id': 'payer:1',
},
)
assert mock_pricing_data.call_args_list == [
@ -626,6 +626,6 @@ def test_pricing_compute_for_flat_fee_schedule_without_subscription(mock_pricing
request=mock.ANY,
pricing_date=datetime.date(2021, 9, 15),
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
),
]

View File

@ -627,10 +627,10 @@ def test_detail_pool_invoices(app, admin_user, draft):
status='completed',
)
invoice1 = invoice_model.objects.create(
date_issue=datetime.date.today(), regie=regie, pool=pool, payer='payer:1'
date_issue=datetime.date.today(), regie=regie, pool=pool, payer_external_id='payer:1'
)
invoice2 = invoice_model.objects.create(
date_issue=datetime.date.today(), regie=regie, pool=pool, payer='payer:2'
date_issue=datetime.date.today(), regie=regie, pool=pool, payer_external_id='payer:2'
)
if not draft:
invoice1.set_number()
@ -648,7 +648,8 @@ def test_detail_pool_invoices(app, admin_user, draft):
pool=pool,
label='Label 11',
user_external_id='user:1',
user_name='User1 Name1',
user_first_name='User1',
user_last_name='Name1',
)
line12 = line_model.objects.create(
event_date=datetime.date(2022, 9, 2),
@ -660,7 +661,8 @@ def test_detail_pool_invoices(app, admin_user, draft):
pool=pool,
label='Label 12',
user_external_id='user:2',
user_name='User2 Name2',
user_first_name='User2',
user_last_name='Name2',
)
line13 = line_model.objects.create(
event_date=datetime.date(2022, 9, 3),
@ -672,7 +674,8 @@ def test_detail_pool_invoices(app, admin_user, draft):
pool=pool,
label='Label 13',
user_external_id='user:1',
user_name='User1 Name1',
user_first_name='User1',
user_last_name='Name1',
)
orphan_line = line_model.objects.create(
@ -684,7 +687,8 @@ def test_detail_pool_invoices(app, admin_user, draft):
pool=pool,
label='Label 14',
user_external_id='user:1',
user_name='User1 Name1',
user_first_name='User1',
user_last_name='Name1',
)
line21 = line_model.objects.create(
@ -697,7 +701,8 @@ def test_detail_pool_invoices(app, admin_user, draft):
pool=pool,
label='Label 21',
user_external_id='user:1',
user_name='User1 Name1',
user_first_name='User1',
user_last_name='Name1',
)
app = login(app)
@ -983,10 +988,10 @@ def test_journal_pool_lines(app, admin_user, draft):
status='completed',
)
invoice1 = invoice_model.objects.create(
date_issue=datetime.date.today(), regie=regie, pool=pool, payer='payer:1'
date_issue=datetime.date.today(), regie=regie, pool=pool, payer_external_id='payer:1'
)
invoice2 = invoice_model.objects.create(
date_issue=datetime.date.today(), regie=regie, pool=pool, payer='payer:2'
date_issue=datetime.date.today(), regie=regie, pool=pool, payer_external_id='payer:2'
)
if not draft:
invoice1.set_number()
@ -1032,6 +1037,15 @@ def test_journal_pool_lines(app, admin_user, draft):
'PricingBookingCheckTypeError',
{'check_type_group': 'foo-bar', 'check_type': 'foo-reason', 'reason': 'wrong-kind'},
),
('PayerError', {'reason': 'empty-template'}),
('PayerError', {'reason': 'empty-result'}),
('PayerError', {'reason': 'syntax-error'}),
('PayerError', {'reason': 'variable-error'}),
('PayerDataError', {'key': 'foo', 'reason': 'empty-template'}),
('PayerDataError', {'key': 'foo', 'reason': 'empty-result'}),
('PayerDataError', {'key': 'foo', 'reason': 'syntax-error'}),
('PayerDataError', {'key': 'foo', 'reason': 'variable-error'}),
('PayerDataError', {'key': 'foo', 'reason': 'not-a-boolean'}),
]
for error, error_details in errors:
lines.append(
@ -1181,41 +1195,115 @@ def test_journal_pool_lines(app, admin_user, draft):
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[10].pk).text().strip()
== "{'error': 'PricingBookingCheckTypeError', 'error_details': {'reason': 'not-found'}} {'event': 'foobar'}"
)
if draft:
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[11].pk).text())
== 'Error (Check type error: pricing not configured (group: foo-bar, check type: foo-reason))%s'
% error_links
)
else:
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[11].pk).text())
== 'Fixed (Check type error: pricing not configured (group: foo-bar, check type: foo-reason)) reset'
)
assert resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[11].pk).text().strip() == (
"{'error': 'PricingBookingCheckTypeError', 'error_details': {'check_type': 'foo-reason', "
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[11].pk).text())
== 'Error (Check type error: pricing not configured (group: foo-bar, check type: foo-reason))%s'
% error_links
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[11].pk).text().strip()
== "{'error': 'PricingBookingCheckTypeError', 'error_details': {'check_type': 'foo-reason', "
"'check_type_group': 'foo-bar', 'reason': 'not-configured'}} {'event': 'foobar'}"
)
if draft:
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[12].pk).text())
== 'Error (Check type error: wrong kind (group: foo-bar, check type: foo-reason))%s' % error_links
)
else:
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[12].pk).text())
== 'Ignored (Check type error: wrong kind (group: foo-bar, check type: foo-reason)) reset'
)
assert resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[12].pk).text().strip() == (
"{'error': 'PricingBookingCheckTypeError', 'error_details': {'check_type': 'foo-reason', "
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[12].pk).text())
== 'Error (Check type error: wrong kind (group: foo-bar, check type: foo-reason))%s' % error_links
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[12].pk).text().strip()
== "{'error': 'PricingBookingCheckTypeError', 'error_details': {'check_type': 'foo-reason', "
"'check_type_group': 'foo-bar', 'reason': 'wrong-kind'}} {'event': 'foobar'}"
)
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[13].pk).text())
== 'Success (Injected)'
== 'Error (Impossible to determine payer: Template is empty)%s' % error_links
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[13].pk).text().strip()
== "{'error': 'PayerError', 'error_details': {'reason': 'empty-template'}} {'event': 'foobar'}"
)
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[14].pk).text())
== 'Error (Impossible to determine payer: Result is empty)%s' % error_links
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[14].pk).text().strip()
== "{'error': 'PayerError', 'error_details': {'reason': 'empty-result'}} {'event': 'foobar'}"
)
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[15].pk).text())
== 'Error (Impossible to determine payer: Syntax error)%s' % error_links
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[15].pk).text().strip()
== "{'error': 'PayerError', 'error_details': {'reason': 'syntax-error'}} {'event': 'foobar'}"
)
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[16].pk).text())
== 'Error (Impossible to determine payer: Variable error)%s' % error_links
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[16].pk).text().strip()
== "{'error': 'PayerError', 'error_details': {'reason': 'variable-error'}} {'event': 'foobar'}"
)
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[17].pk).text())
== 'Error (Impossible to get payer foo: Template is empty)%s' % error_links
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[17].pk).text().strip()
== "{'error': 'PayerDataError', 'error_details': {'key': 'foo', 'reason': 'empty-template'}} {'event': 'foobar'}"
)
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[18].pk).text())
== 'Error (Impossible to get payer foo: Result is empty)%s' % error_links
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[18].pk).text().strip()
== "{'error': 'PayerDataError', 'error_details': {'key': 'foo', 'reason': 'empty-result'}} {'event': 'foobar'}"
)
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[19].pk).text())
== 'Error (Impossible to get payer foo: Syntax error)%s' % error_links
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[19].pk).text().strip()
== "{'error': 'PayerDataError', 'error_details': {'key': 'foo', 'reason': 'syntax-error'}} {'event': 'foobar'}"
)
if draft:
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[20].pk).text())
== 'Error (Impossible to get payer foo: Variable error)%s' % error_links
)
else:
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[20].pk).text())
== 'Fixed (Impossible to get payer foo: Variable error) reset'
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[20].pk).text().strip()
== "{'error': 'PayerDataError', 'error_details': {'key': 'foo', 'reason': 'variable-error'}} {'event': 'foobar'}"
)
if draft:
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[21].pk).text())
== 'Error (Impossible to get payer foo: Result is not a boolean)%s' % error_links
)
else:
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[21].pk).text())
== 'Ignored (Impossible to get payer foo: Result is not a boolean) reset'
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[21].pk).text().strip()
== "{'error': 'PayerDataError', 'error_details': {'key': 'foo', 'reason': 'not-a-boolean'}} {'event': 'foobar'}"
)
assert (
format_status(resp.pyquery('tr[data-line-id="%s"] td.status' % lines[22].pk).text())
== 'Success (Injected)'
)
assert (
resp.pyquery('tr[data-details-for-line-id="%s"] td pre' % lines[22].pk).text().strip()
== "{'foo': 'bar'} {'event': 'foobar2'}"
)
@ -1251,12 +1339,12 @@ def test_journal_pool_lines(app, admin_user, draft):
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/journal/' % (regie.pk, campaign.pk, pool.pk),
params={'payer_external_id': 'payer:2'},
)
assert len(resp.pyquery('tr td.status')) == 13
assert len(resp.pyquery('tr td.status')) == 22
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/journal/' % (regie.pk, campaign.pk, pool.pk),
params={'user_external_id': 'user:1'},
)
assert len(resp.pyquery('tr td.status')) == 13
assert len(resp.pyquery('tr td.status')) == 22
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/journal/' % (regie.pk, campaign.pk, pool.pk),
params={'user_external_id': 'user:2'},
@ -1281,13 +1369,13 @@ def test_journal_pool_lines(app, admin_user, draft):
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/journal/' % (regie.pk, campaign.pk, pool.pk),
params={'status': 'error'},
)
assert len(resp.pyquery('tr td.status')) == 11
assert len(resp.pyquery('tr td.status')) == 20
if not draft:
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/journal/' % (regie.pk, campaign.pk, pool.pk),
params={'status': 'error_todo'},
)
assert len(resp.pyquery('tr td.status')) == 9
assert len(resp.pyquery('tr td.status')) == 18
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/journal/' % (regie.pk, campaign.pk, pool.pk),
params={'status': 'error_ignored'},

File diff suppressed because it is too large Load Diff

View File

@ -841,7 +841,7 @@ def test_detail_agenda_pricing_test_tool_for_event(mock_pricing_data_event, mock
resp.form['agenda'] = agenda.pk
resp.form['event_slug'] = 'foo'
resp.form['user_external_id'] = 'user:1'
resp.form['adult_external_id'] = 'adult:1'
resp.form['payer_external_id'] = 'payer:1'
resp.form['booking_status'] = 'presence'
resp = resp.form.submit().follow()
assert 'Computed pricing data' not in resp
@ -867,7 +867,7 @@ def test_detail_agenda_pricing_test_tool_for_event(mock_pricing_data_event, mock
event={'start_datetime': '2021-09-01T12:00:00+02:00'},
check_status={'status': 'presence', 'check_type': None},
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
)
]
@ -910,7 +910,7 @@ def test_detail_agenda_pricing_test_tool_for_event(mock_pricing_data_event, mock
event={'start_datetime': '2021-09-01T00:00:00+02:00', 'recurrence_days': [1]},
check_status={'status': 'presence', 'check_type': None},
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
)
]
@ -933,7 +933,7 @@ def test_detail_agenda_pricing_test_tool_for_event_event_error(mock_event, app,
resp.form['agenda'] = agenda.pk
resp.form['event_slug'] = 'foo-event'
resp.form['user_external_id'] = 'user:1'
resp.form['adult_external_id'] = 'adult:1'
resp.form['payer_external_id'] = 'payer:1'
resp.form['booking_status'] = 'presence'
resp = resp.form.submit().follow()
assert resp.context['test_tool_form'].errors['event_slug'] == ['foo bar foo-event']
@ -976,7 +976,7 @@ def test_detail_agenda_pricing_test_tool_for_event_booking_status(
resp.form['agenda'] = agenda.pk
resp.form['event_slug'] = 'foo'
resp.form['user_external_id'] = 'user:1'
resp.form['adult_external_id'] = 'adult:1'
resp.form['payer_external_id'] = 'payer:1'
resp.form['booking_status'] = 'presence'
resp = resp.form.submit().follow()
assert resp.form['booking_status'].options == [
@ -1037,7 +1037,7 @@ def test_detail_agenda_pricing_test_tool_for_flat_fee_schedule(mock_pricing_data
resp.form['agenda'] = agenda.pk
resp.form['user_external_id'] = 'user:1'
resp.form['adult_external_id'] = 'adult:1'
resp.form['payer_external_id'] = 'payer:1'
mock_pricing_data.return_value = {'foo': 'bar', 'pricing': Decimal('42')}
resp = resp.form.submit().follow()
assert 'Computed pricing data' in resp
@ -1046,7 +1046,7 @@ def test_detail_agenda_pricing_test_tool_for_flat_fee_schedule(mock_pricing_data
request=mock.ANY,
pricing_date=datetime.date(2021, 9, 1),
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
)
]
assert '<p>Pricing: 42.00</p>' in resp
@ -1088,14 +1088,14 @@ def test_detail_agenda_pricing_test_tool_for_flat_fee_schedule(mock_pricing_data
resp.form['agenda'] = agenda.pk
resp.form['billing_date'] = billing_date1.pk
resp.form['user_external_id'] = 'user:1'
resp.form['adult_external_id'] = 'adult:1'
resp.form['payer_external_id'] = 'payer:1'
resp = resp.form.submit().follow()
assert mock_pricing_data.call_args_list == [
mock.call(
request=mock.ANY,
pricing_date=datetime.date(2021, 9, 1),
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
)
]
@ -1107,7 +1107,7 @@ def test_detail_agenda_pricing_test_tool_for_flat_fee_schedule(mock_pricing_data
request=mock.ANY,
pricing_date=datetime.date(2021, 9, 15),
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
)
]
@ -1119,14 +1119,14 @@ def test_detail_agenda_pricing_test_tool_for_flat_fee_schedule(mock_pricing_data
assert 'agenda' not in resp.context['test_tool_form'].fields
resp.form['billing_date'] = billing_date1.pk
resp.form['user_external_id'] = 'user:1'
resp.form['adult_external_id'] = 'adult:1'
resp.form['payer_external_id'] = 'payer:1'
resp = resp.form.submit().follow()
assert mock_pricing_data.call_args_list == [
mock.call(
request=mock.ANY,
pricing_date=datetime.date(2021, 9, 1),
user_external_id='user:1',
adult_external_id='adult:1',
payer_external_id='payer:1',
)
]

View File

@ -124,7 +124,8 @@ def test_pricing_edit_extra_variables(app, admin_user):
pricing.refresh_from_db()
assert pricing.extra_variables == {'foo': 'bar'}
assert '<label>Extra variables:</label>' in resp.text
assert '<i>foo</i>' in resp
assert '<dt><b>foo:</b></dt>' in resp
assert '<dd><pre>bar</pre></dd>' in resp
resp = resp.click(href='/manage/pricing/model/%s/variable/' % pricing.pk)
assert resp.form['form-TOTAL_FORMS'].value == '2'
@ -141,7 +142,10 @@ def test_pricing_edit_extra_variables(app, admin_user):
'foo': 'bar-bis',
'blah': 'baz',
}
assert '<i>blah</i>, <i>foo</i>' in resp
assert '<dt><b>blah:</b></dt>' in resp
assert '<dd><pre>baz</pre></dd>' in resp
assert '<dt><b>foo:</b></dt>' in resp
assert '<dd><pre>bar-bis</pre></dd>' in resp
resp = resp.click(href='/manage/pricing/model/%s/variable/' % pricing.pk)
assert resp.form['form-TOTAL_FORMS'].value == '3'
@ -159,7 +163,45 @@ def test_pricing_edit_extra_variables(app, admin_user):
assert pricing.extra_variables == {
'foo': 'bar',
}
assert '<i>foo</i>' in resp
assert '<dt><b>foo:</b></dt>' in resp
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_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>') == 5
resp = resp.click(href='/manage/pricing/model/%s/payer/' % pricing.pk)
assert resp.form['form-TOTAL_FORMS'].value == '5'
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_first_name'
assert resp.form['form-1-value'].value == ''
assert resp.form['form-2-key'].value == 'payer_last_name'
assert resp.form['form-2-value'].value == ''
assert resp.form['form-3-key'].value == 'payer_demat'
assert resp.form['form-3-value'].value == ''
assert resp.form['form-4-key'].value == 'payer_direct_debit'
assert resp.form['form-4-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_first_name': '',
'payer_last_name': '',
'payer_demat': '',
'payer_direct_debit': '',
}
def test_pricing_add_category(app, admin_user):

View File

@ -16,6 +16,8 @@ from lingo.pricing.models import (
CriteriaCategory,
CriteriaConditionNotFound,
MultipleDefaultCriteriaCondition,
PayerDataError,
PayerError,
Pricing,
PricingBookingCheckTypeError,
PricingBookingNotCheckedError,
@ -51,7 +53,10 @@ class MockedRequestResponse(mock.Mock):
def mocked_requests_send(request, **kwargs):
data = [{'id': 1, 'fields': {'foo': 'bar'}}, {'id': 2, 'fields': {'foo': 'baz'}}] # fake result
data = [
{'id': 1, 'fields': {'foo': 'bar', 'bar': False}},
{'id': 2, 'fields': {'foo': 'baz', 'bar': True}},
] # fake result
return MockedRequestResponse(content=json.dumps({'data': data}))
@ -433,7 +438,7 @@ def test_get_pricing_context(mock_send, context, nocache):
agenda_pricing.agendas.add(agenda)
assert (
agenda_pricing.get_pricing_context(
request=context['request'], data={}, user_external_id='child:42', adult_external_id='parent:35'
request=context['request'], data={}, user_external_id='child:42', payer_external_id='parent:35'
)
== {}
)
@ -451,7 +456,7 @@ def test_get_pricing_context(mock_send, context, nocache):
'event': {'foo': 42},
}
assert agenda_pricing.get_pricing_context(
request=context['request'], data=data, user_external_id='child:42', adult_external_id='parent:35'
request=context['request'], data=data, user_external_id='child:42', payer_external_id='parent:35'
) == {
'foo': 'bar',
'qf': '42',
@ -460,24 +465,24 @@ def test_get_pricing_context(mock_send, context, nocache):
'event': '42',
}
# user_external_id and adult_external_id can be used in variables
# user_external_id and payer_external_id can be used in variables
pricing.extra_variables = {
'qf': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_id|filter_by:"bar"|filter_value:adult_external_id|list }}',
'qf': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_id|filter_by:"bar"|filter_value:payer_external_id|list }}',
}
pricing.save()
mock_send.reset_mock()
agenda_pricing.get_pricing_context(
request=context['request'], data={}, user_external_id='child:42', adult_external_id='parent:35'
request=context['request'], data={}, user_external_id='child:42', payer_external_id='parent:35'
)
assert 'filter-foo=child%3A42&' in mock_send.call_args_list[0][0][0].url
assert 'filter-bar=parent%3A35&' in mock_send.call_args_list[0][0][0].url
pricing.extra_variables = {
'qf': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_raw_id|filter_by:"bar"|filter_value:adult_external_raw_id|list }}',
'qf': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_raw_id|filter_by:"bar"|filter_value:payer_external_raw_id|list }}',
}
pricing.save()
mock_send.reset_mock()
agenda_pricing.get_pricing_context(
request=context['request'], data={}, user_external_id='child:42', adult_external_id='parent:35'
request=context['request'], data={}, user_external_id='child:42', payer_external_id='parent:35'
)
assert 'filter-foo=42&' in mock_send.call_args_list[0][0][0].url
assert 'filter-bar=35&' in mock_send.call_args_list[0][0][0].url
@ -1030,7 +1035,7 @@ def test_get_pricing_data(context):
request=context['request'],
pricing_date=datetime.date(year=2021, month=9, day=1),
user_external_id='child:42',
adult_external_id='parent:35',
payer_external_id='parent:35',
) == {
'pricing': 42,
'calculation_details': {
@ -1069,7 +1074,7 @@ def test_get_pricing_data_for_event(context):
event={'start_datetime': make_aware(datetime.datetime(2021, 9, 15, 12, 00)).isoformat()},
check_status={'status': 'not-booked'},
user_external_id='child:42',
adult_external_id='parent:35',
payer_external_id='parent:35',
) == {
'pricing': 0,
'calculation_details': {
@ -1683,3 +1688,170 @@ 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_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