Projet

Général

Profil

0001-pricing-empty-app-65976.patch

Lauréline Guérin, 03 juin 2022 14:49

Télécharger (247 ko)

Voir les différences:

Subject: [PATCH] pricing: empty app (#65976)

 .../migrations/0127_remove_pricing_models.py  |   23 +
 chrono/agendas/models.py                      |   27 -
 chrono/manager/forms.py                       |   31 +-
 chrono/manager/static/css/style.scss          |   36 -
 chrono/manager/static/js/chrono.manager.js    |   33 -
 .../manager_events_agenda_settings.html       |   25 -
 .../templates/chrono/manager_home.html        |    3 -
 chrono/manager/utils.py                       |   15 -
 chrono/manager/views.py                       |   63 +-
 chrono/pricing/forms.py                       |  128 --
 .../migrations/0004_remove_pricing_models.py  |   55 +
 chrono/pricing/models.py                      |  592 ------
 .../manager_agenda_pricing_detail.html        |   61 -
 .../pricing/manager_agenda_pricing_form.html  |   35 -
 .../manager_agenda_pricing_matrix_form.html   |   58 -
 .../manager_criteria_category_form.html       |   31 -
 .../chrono/pricing/manager_criteria_form.html |   31 -
 .../chrono/pricing/manager_criteria_list.html |   58 -
 ...anager_pricing_criteria_category_form.html |   31 -
 .../pricing/manager_pricing_detail.html       |   74 -
 .../manager_pricing_duplicate_form.html       |   22 -
 .../chrono/pricing/manager_pricing_form.html  |   31 -
 .../chrono/pricing/manager_pricing_list.html  |   39 -
 .../manager_pricing_variable_form.html        |   44 -
 chrono/pricing/urls.py                        |  149 --
 chrono/pricing/views.py                       |  667 -------
 chrono/settings.py                            |    3 -
 chrono/urls.py                                |    2 -
 tests/manager/test_all.py                     |   51 -
 tests/manager/test_check_type.py              |   74 -
 tests/manager/test_import_export.py           |   15 +-
 tests/pricing/__init__.py                     |    0
 tests/pricing/conftest.py                     |   36 -
 tests/pricing/test_manager.py                 | 1630 -----------------
 tests/pricing/test_models.py                  | 1477 ---------------
 tests/settings.py                             |    1 -
 tests/test_import_export.py                   |  281 ---
 37 files changed, 86 insertions(+), 5846 deletions(-)
 create mode 100644 chrono/agendas/migrations/0127_remove_pricing_models.py
 delete mode 100644 chrono/pricing/forms.py
 create mode 100644 chrono/pricing/migrations/0004_remove_pricing_models.py
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_agenda_pricing_detail.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_agenda_pricing_form.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_agenda_pricing_matrix_form.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_criteria_category_form.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_criteria_form.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_criteria_list.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_pricing_criteria_category_form.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_pricing_detail.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_pricing_duplicate_form.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_pricing_form.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_pricing_list.html
 delete mode 100644 chrono/pricing/templates/chrono/pricing/manager_pricing_variable_form.html
 delete mode 100644 chrono/pricing/urls.py
 delete mode 100644 chrono/pricing/views.py
 delete mode 100644 tests/pricing/__init__.py
 delete mode 100644 tests/pricing/conftest.py
 delete mode 100644 tests/pricing/test_manager.py
 delete mode 100644 tests/pricing/test_models.py
chrono/agendas/migrations/0127_remove_pricing_models.py
1
from django.db import migrations
2

  
3

  
4
class Migration(migrations.Migration):
5

  
6
    dependencies = [
7
        ('agendas', '0126_pricing'),
8
    ]
9

  
10
    operations = [
11
        migrations.RemoveField(
12
            model_name='agenda',
13
            name='pricings',
14
        ),
15
        migrations.RemoveField(
16
            model_name='checktype',
17
            name='pricing',
18
        ),
19
        migrations.RemoveField(
20
            model_name='checktype',
21
            name='pricing_rate',
22
        ),
23
    ]
chrono/agendas/models.py
251 251
        null=True,
252 252
        blank=True,
253 253
    )
254
    pricings = models.ManyToManyField(
255
        'pricing.Pricing',
256
        related_name='agendas',
257
        through='pricing.AgendaPricing',
258
    )
259 254

  
260 255
    class Meta:
261 256
        ordering = ['label']
......
404 399
            agenda['event_display_template'] = self.event_display_template
405 400
            agenda['mark_event_checked_auto'] = self.mark_event_checked_auto
406 401
            agenda['events_type'] = self.events_type.slug if self.events_type else None
407
            agenda['pricings'] = [x.export_json() for x in self.agendapricing_set.all()]
408 402
        elif self.kind == 'meetings':
409 403
            agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.filter(deleted=False)]
410 404
            agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()]
......
423 417
            events = data.pop('events')
424 418
            notifications_settings = data.pop('notifications_settings', None)
425 419
            exceptions_desk = data.pop('exceptions_desk', None)
426
            pricings = data.pop('pricings', None) or []
427 420
        elif data['kind'] == 'meetings':
428 421
            meetingtypes = data.pop('meetingtypes')
429 422
            desks = data.pop('desks')
......
455 448
                data['events_type'] = EventsType.objects.get(slug=data['events_type'])
456 449
            except EventsType.DoesNotExist:
457 450
                raise AgendaImportError(_('Missing "%s" events type') % data['events_type'])
458
        if data['kind'] == 'events' and pricings:
459
            from chrono.pricing.models import Pricing
460

  
461
            for pricing_data in pricings:
462
                try:
463
                    pricing_data['pricing'] = Pricing.objects.get(slug=pricing_data['pricing'])
464
                except Pricing.DoesNotExist:
465
                    raise AgendaImportError(_('Missing "%s" pricing model') % pricing_data['pricing'])
466 451
        agenda, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
467 452
        if overwrite:
468 453
            AgendaReminderSettings.objects.filter(agenda=agenda).delete()
......
473 458
            if overwrite:
474 459
                Event.objects.filter(agenda=agenda).delete()
475 460
                AgendaNotificationsSettings.objects.filter(agenda=agenda).delete()
476
                agenda.agendapricing_set.all().delete()
477 461
            for event_data in events:
478 462
                event_data['agenda'] = agenda
479 463
                Event.import_json(event_data)
480 464
            if notifications_settings:
481 465
                notifications_settings['agenda'] = agenda
482 466
                AgendaNotificationsSettings.import_json(notifications_settings)
483
            for pricing_data in pricings:
484
                from chrono.pricing.models import AgendaPricing
485

  
486
                pricing_data['agenda'] = agenda
487
                AgendaPricing.import_json(pricing_data)
488 467
            if exceptions_desk:
489 468
                exceptions_desk['agenda'] = agenda
490 469
                Desk.import_json(exceptions_desk)
......
3129 3108
        choices=[('absence', _('Absence')), ('presence', _('Presence'))],
3130 3109
        default='absence',
3131 3110
    )
3132
    pricing = models.DecimalField(
3133
        _('Pricing'), max_digits=5, decimal_places=2, help_text=_('Fixed pricing'), blank=True, null=True
3134
    )
3135
    pricing_rate = models.PositiveIntegerField(
3136
        _('Pricing rate'), help_text=_('Percentage rate'), blank=True, null=True
3137
    )
3138 3111
    disabled = models.BooleanField(_('Disabled'), default=False)
3139 3112
    objects = CheckTypeManager()
3140 3113

  
chrono/manager/forms.py
69 69
from .widgets import SplitDateTimeField, WeekdaysWidget
70 70

  
71 71

  
72
class NewCheckTypeForm(forms.ModelForm):
72
class CheckTypeForm(forms.ModelForm):
73 73
    class Meta:
74 74
        model = CheckType
75
        fields = ['label', 'kind', 'pricing', 'pricing_rate']
76

  
77
    def __init__(self, *args, **kwargs):
78
        super().__init__(*args, **kwargs)
79
        if not settings.CHRONO_ENABLE_PRICING:
80
            del self.fields['pricing']
81
            del self.fields['pricing_rate']
82

  
83
    def clean(self):
84
        super().clean()
85
        if self.cleaned_data.get('pricing') is not None and self.cleaned_data.get('pricing_rate') is not None:
86
            raise ValidationError(_('Please choose between pricing and pricing rate.'))
87

  
88

  
89
class CheckTypeForm(NewCheckTypeForm):
90
    class Meta:
91
        model = CheckType
92
        fields = ['label', 'slug', 'pricing', 'pricing_rate', 'disabled']
75
        fields = ['label', 'slug', 'disabled']
93 76

  
94 77
    def clean_slug(self):
95 78
        slug = self.cleaned_data['slug']
......
1425 1408
    categories = forms.BooleanField(label=_('Categories'), required=False, initial=True)
1426 1409
    check_type_groups = forms.BooleanField(label=_('Check type groups'), required=False, initial=True)
1427 1410
    events_types = forms.BooleanField(label=_('Events types'), required=False, initial=True)
1428
    pricing_categories = forms.BooleanField(
1429
        label=_('Pricing criteria categories'), required=False, initial=True
1430
    )
1431
    pricing_models = forms.BooleanField(label=_('Pricing models'), required=False, initial=True)
1432

  
1433
    def __init__(self, *args, **kwargs):
1434
        super().__init__(*args, **kwargs)
1435
        if not settings.CHRONO_ENABLE_PRICING:
1436
            del self.fields['pricing_categories']
1437
            del self.fields['pricing_models']
1438 1411

  
1439 1412

  
1440 1413
class SharedCustodyRuleForm(forms.ModelForm):
chrono/manager/static/css/style.scss
522 522
.page_break {
523 523
	height: 20px;
524 524
}
525

  
526
div.paragraph {
527
	background: white;
528
	box-sizing: border-box;
529
	border: 1px solid #386ede;
530
	border-radius: 3px;
531
	max-width: 100%;
532
	padding: 5px 15px;
533
	margin-bottom: 1rem;
534
	h4 {
535
		margin-top: 5px;
536
		border-bottom: none;
537
	}
538
}
539

  
540
.sortable {
541
	span.handle {
542
		cursor: move;
543
		display: inline-block;
544
		padding: 0.5ex;
545
		text-align: center;
546
		width: 2em;
547
		height: 100%;
548
		box-sizing: border-box;
549
        font-weight: normal;
550
	}
551
}
552

  
553
ul.objects-list.sortable {
554
	li {
555
		position: relative;
556
		& > a {
557
			display: inline-block;
558
		}
559
	}
560
}
chrono/manager/static/js/chrono.manager.js
48 48
      total_form.val(form_num + 1);
49 49
    })
50 50
  }
51

  
52
  $(document).on('click', '#add-pricing-variable-form', function() {
53
    if (typeof property_forms === "undefined") {var property_forms = $('.pricing-variable-form');}
54
    if (typeof total_forms === "undefined") {var total_form = $('#id_form-TOTAL_FORMS');}
55
    if (typeof form_num === "undefined") {var form_num = property_forms.length - 1;}
56
    var new_form = $(property_forms[0]).clone();
57
    var form_regex = RegExp(`form-(\\d){1}-`,'g');
58
    form_num++;
59
    new_form.html(new_form.html().replace(form_regex, `form-${form_num}-`));
60
    new_form.appendTo('#pricing-variable-forms tbody');
61
    $('#id_form-' + form_num + '-key').val('');
62
    $('#id_form-' + form_num + '-value').val('');
63
    total_form.val(form_num + 1);
64
  })
65

  
66
  $('.sortable').sortable({
67
    handle: '.handle',
68
    items: '.sortable-item',
69
    update : function(event, ui) {
70
      var new_order = '';
71
      $(this).find('.sortable-item').each(function(i, x) {
72
        var item_id = $(x).data('item-id');
73
        if (new_order) {
74
          new_order += ',';
75
        }
76
        new_order += item_id;
77
      });
78
      $.ajax({
79
        url: $(this).data('order-url'),
80
        data: {'new-order': new_order}
81
      });
82
    }
83
  });
84 51
});
chrono/manager/templates/chrono/manager_events_agenda_settings.html
4 4
{% block agenda-extra-management-actions %}
5 5
  <a rel="popup" href="{% url 'chrono-manager-agenda-import-events' pk=object.id %}">{% trans 'Import Events' %}</a>
6 6
  <a rel="popup" href="{% url 'chrono-manager-agenda-add-event' pk=object.id %}">{% trans 'New Event' %}</a>
7
  {% if pricing_enabled %}
8
  <a rel="popup" href="{% url 'chrono-manager-agenda-pricing-add' pk=object.id %}">{% trans 'New pricing' %}</a>
9
  {% endif %}
10 7
{% endblock %}
11 8

  
12 9
{% block agenda-settings %}
......
93 90
</div>
94 91
</div>
95 92

  
96
{% if pricing_enabled %}
97
<div class="section">
98
<h3>{% trans "Pricing" context 'pricing' %}</h3>
99
<div>
100
  {% if agenda_pricings %}
101
  <ul class="objects-list single-links">
102
  {% for agenda_pricing in agenda_pricings %}
103
    <li><a href="{% url 'chrono-manager-agenda-pricing-detail' agenda.pk agenda_pricing.pk %}">{{ agenda_pricing.pricing }} ({{ agenda_pricing.date_start|date:'d/m/Y' }} - {{ agenda_pricing.date_end|date:'d/m/Y' }})</a></li>
104
  {% endfor %}
105
  </ul>
106
  {% else %}
107
  <div class="big-msg-info">
108
    {% blocktrans %}
109
    This agenda doesn't have any pricing defined yet. Click on the "New pricing" button in
110
    the top right of the page to add a first one.
111
    {% endblocktrans %}
112
  </div>
113
  {% endif %}
114
</div>
115
</div>
116
{% endif %}
117

  
118 93
<div class="section">
119 94
<h3>{% trans "Notifications" %}
120 95
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-notifications-settings' pk=object.id %}">{% trans 'Configure' %}</a>
chrono/manager/templates/chrono/manager_home.html
12 12
    <li><a rel="popup" href="{% url 'chrono-manager-agendas-export' %}" data-autoclose-dialog="true">{% trans 'Export' %}</a></li>
13 13
    <li><a href="{% url 'chrono-manager-events-type-list' %}">{% trans 'Events types' %}</a></li>
14 14
    <li><a href="{% url 'chrono-manager-check-type-list' %}">{% trans 'Check types' %}</a></li>
15
    {% if pricing_enabled %}
16
    <li><a href="{% url 'chrono-manager-pricing-list' %}">{% trans 'Pricing' context 'pricing' %}</a></li>
17
    {% endif %}
18 15
    {% endif %}
19 16
    {% if has_access_to_unavailability_calendars %}
20 17
    <li><a href="{% url 'chrono-manager-unavailability-calendar-list' %}">{% trans 'Unavailability calendars' %}</a></li>
chrono/manager/utils.py
30 30
    Resource,
31 31
    UnavailabilityCalendar,
32 32
)
33
from chrono.pricing.models import CriteriaCategory, Pricing
34 33

  
35 34

  
36 35
def export_site(
......
40 39
    events_types=True,
41 40
    resources=True,
42 41
    categories=True,
43
    pricing_categories=True,
44
    pricing_models=True,
45 42
):
46 43
    '''Dump site objects to JSON-dumpable dictionnary'''
47 44
    data = collections.OrderedDict()
48
    if pricing_models:
49
        data['pricing_models'] = [x.export_json() for x in Pricing.objects.all()]
50
    if pricing_categories:
51
        data['pricing_categories'] = [x.export_json() for x in CriteriaCategory.objects.all()]
52 45
    if categories:
53 46
        data['categories'] = [x.export_json() for x in Category.objects.all()]
54 47
    if resources:
......
75 68
        or EventsType.objects.exists()
76 69
        or Resource.objects.exists()
77 70
        or Category.objects.exists()
78
        or CriteriaCategory.objects.exists()
79
        or Pricing.objects.exists()
80 71
    ):
81 72
        return
82 73

  
......
87 78
        EventsType.objects.all().delete()
88 79
        Resource.objects.all().delete()
89 80
        Category.objects.all().delete()
90
        CriteriaCategory.objects.all().delete()
91
        Pricing.objects.all().delete()
92 81

  
93 82
    results = {
94 83
        key: collections.defaultdict(list)
......
99 88
            'events_types',
100 89
            'resources',
101 90
            'categories',
102
            'pricing_categories',
103
            'pricing_models',
104 91
        ]
105 92
    }
106 93

  
......
124 111
            (CheckTypeGroup, 'check_type_groups'),
125 112
            (UnavailabilityCalendar, 'unavailability_calendars'),
126 113
            (Agenda, 'agendas'),
127
            (CriteriaCategory, 'pricing_categories'),
128
            (Pricing, 'pricing_models'),
129 114
        ):
130 115
            objs = data.get(key, [])
131 116
            for obj in objs:
chrono/manager/views.py
26 26

  
27 27
import requests
28 28
from dateutil.relativedelta import MO, relativedelta
29
from django.conf import settings
30 29
from django.contrib import messages
31 30
from django.core.exceptions import PermissionDenied
32 31
from django.db import IntegrityError, transaction
......
87 86
    UnavailabilityCalendar,
88 87
    VirtualMember,
89 88
)
90
from chrono.pricing.models import AgendaPricing
91 89
from chrono.utils.date import get_weekday_index
92 90

  
93 91
from .forms import (
......
116 114
    EventsTimesheetForm,
117 115
    ImportEventsForm,
118 116
    MeetingTypeForm,
119
    NewCheckTypeForm,
120 117
    NewDeskForm,
121 118
    NewEventForm,
122 119
    NewMeetingTypeForm,
......
165 162
    def get_context_data(self, **kwargs):
166 163
        context = super().get_context_data(**kwargs)
167 164
        context['has_access_to_unavailability_calendars'] = self.has_access_to_unavailability_calendars()
168
        context['pricing_enabled'] = settings.CHRONO_ENABLE_PRICING
169 165
        return context
170 166

  
171 167
    def get(self, request, *args, **kwargs):
......
763 759
class CheckTypeAddView(CreateView):
764 760
    template_name = 'chrono/manager_check_type_form.html'
765 761
    model = CheckType
766
    form_class = NewCheckTypeForm
762
    fields = ['label', 'kind']
767 763

  
768 764
    def dispatch(self, request, *args, **kwargs):
769 765
        self.group_pk = kwargs.pop('group_pk')
......
1073 1069
                    x,
1074 1070
                ),
1075 1071
            },
1076
            'pricing_categories': {
1077
                'create_noop': _('No pricing criteria category created.'),
1078
                'create': lambda x: ungettext(
1079
                    'A pricing criteria category has been created.',
1080
                    '%(count)d pricing criteria categories have been created.',
1081
                    x,
1082
                ),
1083
                'update_noop': _('No pricing criteria category updated.'),
1084
                'update': lambda x: ungettext(
1085
                    'A pricing criteria category has been updated.',
1086
                    '%(count)d pricing criteria categories have been updated.',
1087
                    x,
1088
                ),
1089
            },
1090
            'pricing_models': {
1091
                'create_noop': _('No pricing model created.'),
1092
                'create': lambda x: ungettext(
1093
                    'A pricing model has been created.',
1094
                    '%(count)d pricing models have been created.',
1095
                    x,
1096
                ),
1097
                'update_noop': _('No pricing model updated.'),
1098
                'update': lambda x: ungettext(
1099
                    'A pricing model has been updated.',
1100
                    '%(count)d pricing models have been updated.',
1101
                    x,
1102
                ),
1103
            },
1104 1072
        }
1105 1073

  
1106 1074
        global_noop = True
......
1121 1089

  
1122 1090
                obj_results['messages'] = "%s %s" % (message1, message2)
1123 1091

  
1124
        a_count, uc_count, arg_count, pc_count, pm_count = (
1092
        a_count, uc_count, arg_count = (
1125 1093
            len(results['agendas']['all']),
1126 1094
            len(results['unavailability_calendars']['all']),
1127 1095
            len(results['check_type_groups']['all']),
1128
            len(results['pricing_categories']['all']),
1129
            len(results['pricing_models']['all']),
1130 1096
        )
1131
        if (a_count, uc_count, arg_count, pc_count, pm_count) == (1, 0, 0, 0, 0):
1097
        if (a_count, uc_count, arg_count) == (1, 0, 0):
1132 1098
            # only one agenda imported, redirect to settings page
1133 1099
            return HttpResponseRedirect(
1134 1100
                reverse('chrono-manager-agenda-settings', kwargs={'pk': results['agendas']['all'][0].pk})
1135 1101
            )
1136
        if (a_count, uc_count, arg_count, pc_count, pm_count) == (0, 1, 0, 0, 0):
1102
        if (a_count, uc_count, arg_count) == (0, 1, 0):
1137 1103
            # only one unavailability calendar imported, redirect to settings page
1138 1104
            return HttpResponseRedirect(
1139 1105
                reverse(
......
1141 1107
                    kwargs={'pk': results['unavailability_calendars']['all'][0].pk},
1142 1108
                )
1143 1109
            )
1144
        if (a_count, uc_count, arg_count, pc_count, pm_count) == (0, 0, 1, 0, 0):
1110
        if (a_count, uc_count, arg_count) == (0, 0, 1):
1145 1111
            # only one check type group imported, redirect to check type page
1146 1112
            return HttpResponseRedirect(reverse('chrono-manager-check-type-list'))
1147
        if (a_count, uc_count, arg_count, pc_count, pm_count) == (0, 0, 0, 1, 0):
1148
            # only one criteria category imported, redirect to criteria page
1149
            return HttpResponseRedirect(reverse('chrono-manager-pricing-criteria-list'))
1150
        if (a_count, uc_count, arg_count, pc_count, pm_count) == (0, 0, 0, 0, 1):
1151
            # only one pricing imported, redirect to pricing page
1152
            return HttpResponseRedirect(
1153
                reverse(
1154
                    'chrono-manager-pricing-detail',
1155
                    kwargs={'pk': results['pricing_models']['all'][0].pk},
1156
                )
1157
            )
1158 1113

  
1159 1114
        if global_noop:
1160 1115
            messages.info(self.request, _('No data found.'))
......
1165 1120
            messages.info(self.request, results['events_types']['messages'])
1166 1121
            messages.info(self.request, results['resources']['messages'])
1167 1122
            messages.info(self.request, results['categories']['messages'])
1168
            messages.info(self.request, results['pricing_categories']['messages'])
1169
            messages.info(self.request, results['pricing_models']['messages'])
1170 1123

  
1171 1124
        return super().form_valid(form)
1172 1125

  
......
1925 1878
                end_datetime__gt=now(),
1926 1879
            )
1927 1880
            context['desk'] = desk
1928
            context['pricing_enabled'] = settings.CHRONO_ENABLE_PRICING
1929
            context['agenda_pricings'] = (
1930
                AgendaPricing.objects.filter(agenda=self.agenda)
1931
                .select_related('pricing')
1932
                .order_by('date_start', 'date_end')
1933
            )
1934 1881
        return context
1935 1882

  
1936 1883
    def get_events(self):
chrono/pricing/forms.py
1
# chrono - agendas system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django import forms
18
from django.forms import ValidationError
19
from django.template import Template, TemplateSyntaxError
20
from django.utils.translation import ugettext_lazy as _
21

  
22
from chrono.pricing.models import AgendaPricing, Criteria, CriteriaCategory
23

  
24

  
25
class NewCriteriaForm(forms.ModelForm):
26
    class Meta:
27
        model = Criteria
28
        fields = ['label', 'condition']
29

  
30
    def clean_condition(self):
31
        condition = self.cleaned_data['condition']
32
        try:
33
            Template('{%% if %s %%}OK{%% endif %%}' % condition)
34
        except TemplateSyntaxError:
35
            raise ValidationError(_('Invalid syntax.'))
36

  
37
        return condition
38

  
39

  
40
class CriteriaForm(NewCriteriaForm):
41
    class Meta:
42
        model = Criteria
43
        fields = ['label', 'slug', 'condition']
44

  
45
    def clean_slug(self):
46
        slug = self.cleaned_data['slug']
47

  
48
        if self.instance.category.criterias.filter(slug=slug).exclude(pk=self.instance.pk).exists():
49
            raise ValidationError(_('Another criteria exists with the same identifier.'))
50

  
51
        return slug
52

  
53

  
54
class PricingDuplicateForm(forms.Form):
55
    label = forms.CharField(label=_('New label'), max_length=150, required=False)
56

  
57

  
58
class PricingVariableForm(forms.Form):
59
    key = forms.CharField(label=_('Variable name'), required=False)
60
    value = forms.CharField(
61
        label=_('Value template'), widget=forms.TextInput(attrs={'size': 60}), required=False
62
    )
63

  
64

  
65
PricingVariableFormSet = forms.formset_factory(PricingVariableForm)
66

  
67

  
68
class PricingCriteriaCategoryAddForm(forms.Form):
69
    category = forms.ModelChoiceField(
70
        label=_('Criteria category to add'), queryset=CriteriaCategory.objects.none(), required=True
71
    )
72

  
73
    def __init__(self, *args, **kwargs):
74
        self.pricing = kwargs.pop('pricing')
75
        super().__init__(*args, **kwargs)
76
        self.fields['category'].queryset = CriteriaCategory.objects.exclude(pricings=self.pricing)
77

  
78

  
79
class PricingCriteriaCategoryEditForm(forms.Form):
80
    criterias = forms.ModelMultipleChoiceField(
81
        label=_('Criterias'),
82
        queryset=Criteria.objects.none(),
83
        required=True,
84
        widget=forms.CheckboxSelectMultiple(),
85
    )
86

  
87
    def __init__(self, *args, **kwargs):
88
        self.pricing = kwargs.pop('pricing')
89
        self.category = kwargs.pop('category')
90
        super().__init__(*args, **kwargs)
91
        self.fields['criterias'].queryset = self.category.criterias.all()
92
        self.initial['criterias'] = self.pricing.criterias.filter(category=self.category)
93

  
94

  
95
class AgendaPricingForm(forms.ModelForm):
96
    class Meta:
97
        model = AgendaPricing
98
        fields = ['pricing', 'date_start', 'date_end']
99
        widgets = {
100
            'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
101
            'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
102
        }
103

  
104
    def clean(self):
105
        cleaned_data = super().clean()
106

  
107
        if 'date_start' in cleaned_data and 'date_end' in cleaned_data:
108
            if cleaned_data['date_end'] <= cleaned_data['date_start']:
109
                self.add_error('date_end', _('End date must be greater than start date.'))
110
            else:
111
                overlapping_qs = AgendaPricing.objects.filter(agenda=self.instance.agenda,).extra(
112
                    where=["(date_start, date_end) OVERLAPS (%s, %s)"],
113
                    params=[cleaned_data['date_start'], cleaned_data['date_end']],
114
                )
115
                if self.instance.pk:
116
                    overlapping_qs = overlapping_qs.exclude(pk=self.instance.pk)
117
                if overlapping_qs.exists():
118
                    raise forms.ValidationError(_('Pricing overlaps existing pricings.'))
119

  
120
        return cleaned_data
121

  
122

  
123
class PricingMatrixForm(forms.Form):
124
    def __init__(self, *args, **kwargs):
125
        matrix = kwargs.pop('matrix')
126
        super().__init__(*args, **kwargs)
127
        for i in range(len(matrix.rows[0].cells)):
128
            self.fields['crit_%i' % i] = forms.DecimalField(required=True, max_digits=5, decimal_places=2)
chrono/pricing/migrations/0004_remove_pricing_models.py
1
from django.db import migrations
2

  
3

  
4
class Migration(migrations.Migration):
5

  
6
    dependencies = [
7
        ('agendas', '0127_remove_pricing_models'),
8
        ('pricing', '0003_extra_variables'),
9
    ]
10

  
11
    operations = [
12
        migrations.AlterUniqueTogether(
13
            name='criteria',
14
            unique_together=None,
15
        ),
16
        migrations.RemoveField(
17
            model_name='criteria',
18
            name='category',
19
        ),
20
        migrations.RemoveField(
21
            model_name='pricing',
22
            name='categories',
23
        ),
24
        migrations.RemoveField(
25
            model_name='pricing',
26
            name='criterias',
27
        ),
28
        migrations.AlterUniqueTogether(
29
            name='pricingcriteriacategory',
30
            unique_together=None,
31
        ),
32
        migrations.RemoveField(
33
            model_name='pricingcriteriacategory',
34
            name='category',
35
        ),
36
        migrations.RemoveField(
37
            model_name='pricingcriteriacategory',
38
            name='pricing',
39
        ),
40
        migrations.DeleteModel(
41
            name='AgendaPricing',
42
        ),
43
        migrations.DeleteModel(
44
            name='Criteria',
45
        ),
46
        migrations.DeleteModel(
47
            name='CriteriaCategory',
48
        ),
49
        migrations.DeleteModel(
50
            name='Pricing',
51
        ),
52
        migrations.DeleteModel(
53
            name='PricingCriteriaCategory',
54
        ),
55
    ]
chrono/pricing/models.py
1
# chrono - agendas system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import copy
18
import dataclasses
19
import decimal
20
from typing import List
21

  
22
from django.contrib.postgres.fields import JSONField
23
from django.db import models
24
from django.template import Context, RequestContext, Template, TemplateSyntaxError
25
from django.utils.text import slugify
26
from django.utils.translation import ugettext_lazy as _
27

  
28
from chrono.agendas.models import Agenda, Booking, Subscription
29
from chrono.utils.misc import AgendaImportError, clean_import_data, generate_slug
30

  
31

  
32
class PricingError(Exception):
33
    def __init__(self, details=None):
34
        self.details = details or {}
35
        super().__init__()
36

  
37

  
38
class AgendaPricingNotFound(PricingError):
39
    pass
40

  
41

  
42
class CriteriaConditionNotFound(PricingError):
43
    pass
44

  
45

  
46
class PricingDataError(PricingError):
47
    pass
48

  
49

  
50
class PricingDataFormatError(PricingError):
51
    pass
52

  
53

  
54
class PricingEventNotCheckedError(PricingError):
55
    pass
56

  
57

  
58
class PricingBookingNotCheckedError(PricingError):
59
    pass
60

  
61

  
62
class PricingSubscriptionError(PricingError):
63
    pass
64

  
65

  
66
class PricingMultipleBookingError(PricingError):
67
    pass
68

  
69

  
70
class PricingBookingCheckTypeError(PricingError):
71
    pass
72

  
73

  
74
class CriteriaCategory(models.Model):
75
    label = models.CharField(_('Label'), max_length=150)
76
    slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
77

  
78
    class Meta:
79
        ordering = ['label']
80

  
81
    def __str__(self):
82
        return self.label
83

  
84
    def save(self, *args, **kwargs):
85
        if not self.slug:
86
            self.slug = generate_slug(self)
87
        super().save(*args, **kwargs)
88

  
89
    @property
90
    def base_slug(self):
91
        return slugify(self.label)
92

  
93
    @classmethod
94
    def import_json(cls, data, overwrite=False):
95
        criterias = data.pop('criterias', [])
96
        data = clean_import_data(cls, data)
97
        category, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
98

  
99
        if overwrite:
100
            Criteria.objects.filter(category=category).delete()
101

  
102
        for criteria in criterias:
103
            criteria['category'] = category
104
            Criteria.import_json(criteria)
105

  
106
        return created, category
107

  
108
    def export_json(self):
109
        return {
110
            'label': self.label,
111
            'slug': self.slug,
112
            'criterias': [a.export_json() for a in self.criterias.all()],
113
        }
114

  
115

  
116
class Criteria(models.Model):
117
    category = models.ForeignKey(
118
        CriteriaCategory, verbose_name=_('Category'), on_delete=models.CASCADE, related_name='criterias'
119
    )
120
    label = models.CharField(_('Label'), max_length=150)
121
    slug = models.SlugField(_('Identifier'), max_length=160)
122
    condition = models.CharField(_('Condition'), max_length=1000)
123
    order = models.PositiveIntegerField()
124

  
125
    class Meta:
126
        ordering = ['order']
127
        unique_together = ['category', 'slug']
128

  
129
    def __str__(self):
130
        return self.label
131

  
132
    def save(self, *args, **kwargs):
133
        if self.order is None:
134
            max_order = (
135
                Criteria.objects.filter(category=self.category)
136
                .aggregate(models.Max('order'))
137
                .get('order__max')
138
                or 0
139
            )
140
            self.order = max_order + 1
141
        if not self.slug:
142
            self.slug = generate_slug(self, category=self.category)
143
        super().save(*args, **kwargs)
144

  
145
    @property
146
    def base_slug(self):
147
        return slugify(self.label)
148

  
149
    @classmethod
150
    def import_json(cls, data):
151
        data = clean_import_data(cls, data)
152
        cls.objects.update_or_create(slug=data['slug'], category=data['category'], defaults=data)
153

  
154
    def export_json(self):
155
        return {
156
            'label': self.label,
157
            'slug': self.slug,
158
            'condition': self.condition,
159
            'order': self.order,
160
        }
161

  
162
    @property
163
    def identifier(self):
164
        return '%s:%s' % (self.category.slug, self.slug)
165

  
166
    def compute_condition(self, context):
167
        try:
168
            template = Template('{%% if %s %%}OK{%% endif %%}' % self.condition)
169
        except TemplateSyntaxError:
170
            return False
171
        return template.render(Context(context)) == 'OK'
172

  
173

  
174
class Pricing(models.Model):
175
    label = models.CharField(_('Label'), max_length=150)
176
    slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
177
    categories = models.ManyToManyField(
178
        CriteriaCategory,
179
        related_name='pricings',
180
        through='PricingCriteriaCategory',
181
    )
182
    criterias = models.ManyToManyField(Criteria)
183
    extra_variables = JSONField(blank=True, default=dict)
184

  
185
    class Meta:
186
        ordering = ['label']
187

  
188
    def __str__(self):
189
        return self.label
190

  
191
    def save(self, *args, **kwargs):
192
        if not self.slug:
193
            self.slug = generate_slug(self)
194
        super().save(*args, **kwargs)
195

  
196
    @property
197
    def base_slug(self):
198
        return slugify(self.label)
199

  
200
    def get_extra_variables(self, request, original_context):
201
        result = {}
202
        context = RequestContext(request)
203
        context.push(original_context)
204
        for key, tplt in (self.extra_variables or {}).items():
205
            try:
206
                template = Template(tplt)
207
            except TemplateSyntaxError:
208
                continue
209
            result[key] = template.render(context)
210
        return result
211

  
212
    def get_extra_variables_keys(self):
213
        return sorted((self.extra_variables or {}).keys())
214

  
215
    @classmethod
216
    def import_json(cls, data, overwrite=False):
217
        data = data.copy()
218
        categories = data.pop('categories', [])
219
        categories_by_slug = {c.slug: c for c in CriteriaCategory.objects.all()}
220
        criterias_by_categories_and_slug = {
221
            (crit.category.slug, crit.slug): crit
222
            for crit in Criteria.objects.select_related('category').all()
223
        }
224
        for category_data in categories:
225
            category_slug = category_data['category']
226
            if category_data['category'] not in categories_by_slug:
227
                raise AgendaImportError(_('Missing "%s" pricing category') % category_data['category'])
228
            for criteria_slug in category_data['criterias']:
229
                if (category_slug, criteria_slug) not in criterias_by_categories_and_slug:
230
                    raise AgendaImportError(
231
                        _('Missing "%s" pricing criteria for "%s" category') % (criteria_slug, category_slug)
232
                    )
233
        data = clean_import_data(cls, data)
234
        pricing, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
235

  
236
        PricingCriteriaCategory.objects.filter(pricing=pricing).delete()
237
        criterias = []
238
        for category_data in categories:
239
            pricing.categories.add(
240
                categories_by_slug[category_data['category']],
241
                through_defaults={'order': category_data['order']},
242
            )
243
            for criteria_slug in category_data['criterias']:
244
                criterias.append(criterias_by_categories_and_slug[(category_data['category'], criteria_slug)])
245
        pricing.criterias.set(criterias)
246

  
247
        return created, pricing
248

  
249
    def export_json(self):
250
        return {
251
            'label': self.label,
252
            'slug': self.slug,
253
            'extra_variables': self.extra_variables,
254
            'categories': [pcc.export_json() for pcc in PricingCriteriaCategory.objects.filter(pricing=self)],
255
        }
256

  
257
    def duplicate(self, label=None):
258
        # clone current pricing
259
        new_pricing = copy.deepcopy(self)
260
        new_pricing.pk = None
261
        new_pricing.label = label or _('Copy of %s') % self.label
262
        # reset slug
263
        new_pricing.slug = None
264
        new_pricing.save()
265

  
266
        # set criterias
267
        new_pricing.criterias.set(self.criterias.all())
268

  
269
        # set categories
270
        for pcc in PricingCriteriaCategory.objects.filter(pricing=self):
271
            pcc.duplicate(pricing_target=new_pricing)
272

  
273
        return new_pricing
274

  
275

  
276
class PricingCriteriaCategory(models.Model):
277
    pricing = models.ForeignKey(Pricing, on_delete=models.CASCADE)
278
    category = models.ForeignKey(CriteriaCategory, on_delete=models.CASCADE)
279
    order = models.PositiveIntegerField()
280

  
281
    class Meta:
282
        ordering = ['order']
283
        unique_together = ['pricing', 'category']
284

  
285
    def save(self, *args, **kwargs):
286
        if self.order is None:
287
            max_order = (
288
                PricingCriteriaCategory.objects.filter(pricing=self.pricing)
289
                .aggregate(models.Max('order'))
290
                .get('order__max')
291
                or 0
292
            )
293
            self.order = max_order + 1
294
        super().save(*args, **kwargs)
295

  
296
    def export_json(self):
297
        return {
298
            'category': self.category.slug,
299
            'order': self.order,
300
            'criterias': [c.slug for c in self.pricing.criterias.all() if c.category == self.category],
301
        }
302

  
303
    def duplicate(self, pricing_target):
304
        new_pcc = copy.deepcopy(self)
305
        new_pcc.pk = None
306
        new_pcc.pricing = pricing_target
307
        new_pcc.save()
308
        return new_pcc
309

  
310

  
311
@dataclasses.dataclass
312
class PricingMatrixCell:
313
    criteria: Criteria
314
    value: decimal.Decimal
315

  
316

  
317
@dataclasses.dataclass
318
class PricingMatrixRow:
319
    criteria: Criteria
320
    cells: List[PricingMatrixCell]
321

  
322

  
323
@dataclasses.dataclass
324
class PricingMatrix:
325
    criteria: Criteria
326
    rows: List[PricingMatrixRow]
327

  
328

  
329
class AgendaPricing(models.Model):
330
    agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
331
    pricing = models.ForeignKey(Pricing, on_delete=models.CASCADE)
332
    date_start = models.DateField()
333
    date_end = models.DateField()
334
    pricing_data = JSONField(null=True)
335

  
336
    @classmethod
337
    def import_json(cls, data):
338
        data = clean_import_data(cls, data)
339
        cls.objects.update_or_create(
340
            agenda=data['agenda'],
341
            pricing=data['pricing'],
342
            date_start=data['date_start'],
343
            date_end=data['date_end'],
344
            defaults=data,
345
        )
346

  
347
    def export_json(self):
348
        return {
349
            'pricing': self.pricing.slug,
350
            'date_start': self.date_start.strftime('%Y-%m-%d'),
351
            'date_end': self.date_end.strftime('%Y-%m-%d'),
352
            'pricing_data': self.pricing_data,
353
        }
354

  
355
    @staticmethod
356
    def get_pricing_data(request, event, user_external_id, adult_external_id):
357
        agenda_pricing = AgendaPricing.get_agenda_pricing(event)
358
        context = agenda_pricing.get_pricing_context(request, user_external_id, adult_external_id)
359
        pricing, criterias = agenda_pricing.compute_pricing(context)
360
        modifier = agenda_pricing.get_booking_modifier(event, user_external_id)
361
        return agenda_pricing.aggregate_pricing_data(pricing, criterias, context, modifier)
362

  
363
    def aggregate_pricing_data(self, pricing, criterias, context, modifier):
364
        if modifier['modifier_type'] == 'fixed':
365
            pricing_amount = modifier['modifier_fixed']
366
        else:
367
            pricing_amount = pricing * modifier['modifier_rate'] / 100
368
        return {
369
            'pricing': pricing_amount,
370
            'calculation_details': {
371
                'pricing': pricing,
372
                'criterias': criterias,
373
                'context': context,
374
            },
375
            'booking_details': modifier,
376
        }
377

  
378
    @staticmethod
379
    def get_agenda_pricing(event):
380
        agenda = event.agenda
381
        try:
382
            return agenda.agendapricing_set.get(
383
                date_start__lte=event.start_datetime,
384
                date_end__gt=event.start_datetime,
385
            )
386
        except (AgendaPricing.DoesNotExist, AgendaPricing.MultipleObjectsReturned):
387
            raise AgendaPricingNotFound
388

  
389
    def get_subscription(self, event, user_external_id):
390
        try:
391
            return self.agenda.subscriptions.get(
392
                user_external_id=user_external_id,
393
                date_start__lte=event.start_datetime,
394
                date_end__gt=event.start_datetime,
395
            )
396
        except (Subscription.DoesNotExist, Subscription.MultipleObjectsReturned):
397
            raise PricingSubscriptionError
398

  
399
    def get_pricing_context(self, request, user_external_id, adult_external_id):
400
        context = {'user_external_id': user_external_id, 'adult_external_id': adult_external_id}
401
        if ':' in user_external_id:
402
            context['user_external_raw_id'] = user_external_id.split(':')[1]
403
        if ':' in adult_external_id:
404
            context['adult_external_raw_id'] = adult_external_id.split(':')[1]
405
        return self.pricing.get_extra_variables(request, context)
406

  
407
    def compute_pricing(self, context):
408
        criterias = {}
409
        categories = []
410
        # for each category (ordered)
411
        for category in self.pricing.categories.all().order_by('pricingcriteriacategory__order'):
412
            criterias[category.slug] = None
413
            categories.append(category.slug)
414
            # find the first matching criteria (criterias are ordered)
415
            for criteria in self.pricing.criterias.filter(category=category):
416
                condition = criteria.compute_condition(context)
417
                if condition:
418
                    criterias[category.slug] = criteria.slug
419
                    break
420

  
421
        # now search for pricing values matching found criterias
422
        pricing_data = self.pricing_data
423
        # for each category (ordered)
424
        for category in categories:
425
            criteria = criterias[category]
426
            if criteria is None:
427
                raise CriteriaConditionNotFound(details={'category': category})
428
            if not isinstance(pricing_data, dict):
429
                raise PricingDataFormatError(
430
                    details={'category': category, 'pricing': pricing_data, 'wanted': 'dict'}
431
                )
432
            key = '%s:%s' % (category, criteria)
433
            if key not in pricing_data:
434
                raise PricingDataError(details={'category': category, 'criteria': criteria})
435
            pricing_data = pricing_data[key]
436

  
437
        try:
438
            pricing = decimal.Decimal(pricing_data)
439
        except (decimal.InvalidOperation, ValueError, TypeError):
440
            raise PricingDataFormatError(details={'pricing': pricing_data, 'wanted': 'decimal'})
441

  
442
        return pricing, criterias
443

  
444
    def get_booking_modifier(self, event, user_external_id):
445
        # event must be checked
446
        if event.checked is False:
447
            raise PricingEventNotCheckedError
448

  
449
        # search for an available subscription
450
        self.get_subscription(event, user_external_id)
451

  
452
        # search for a booking
453
        try:
454
            booking = event.booking_set.get(user_external_id=user_external_id)
455
        except Booking.DoesNotExist:
456
            # no booking
457
            return {
458
                'status': 'not-booked',
459
                'modifier_type': 'rate',
460
                'modifier_rate': 0,
461
            }
462
        except Booking.MultipleObjectsReturned:
463
            raise PricingMultipleBookingError
464

  
465
        # booking cancelled
466
        if booking.cancellation_datetime is not None:
467
            return {
468
                'status': 'cancelled',
469
                'modifier_type': 'rate',
470
                'modifier_rate': 0,
471
            }
472

  
473
        # booking not cancelled, is must be checked
474
        if booking.user_was_present is None:
475
            raise PricingBookingNotCheckedError
476

  
477
        status = 'presence' if booking.user_was_present else 'absence'
478
        # no check_type, default rates
479
        if booking.user_check_type is None:
480
            return {
481
                'status': status,
482
                'check_type_group': None,
483
                'check_type': None,
484
                'modifier_type': 'rate',
485
                'modifier_rate': 100 if booking.user_was_present else 0,
486
            }
487

  
488
        check_type = booking.user_check_type
489
        kind_mapping = {'presence': True, 'absence': False}
490
        # check_type kind and user_was_present mismatch
491
        if kind_mapping[check_type.kind] != booking.user_was_present:
492
            raise PricingBookingCheckTypeError(
493
                details={
494
                    'check_type_group': check_type.group.slug,
495
                    'check_type': check_type.slug,
496
                    'reason': 'wrong-kind',
497
                }
498
            )
499

  
500
        # get pricing modifier
501
        if check_type.pricing is not None:
502
            return {
503
                'status': status,
504
                'check_type_group': check_type.group.slug,
505
                'check_type': check_type.slug,
506
                'modifier_type': 'fixed',
507
                'modifier_fixed': check_type.pricing,
508
            }
509
        if check_type.pricing_rate is not None:
510
            return {
511
                'status': status,
512
                'check_type_group': check_type.group.slug,
513
                'check_type': check_type.slug,
514
                'modifier_type': 'rate',
515
                'modifier_rate': check_type.pricing_rate,
516
            }
517
        # pricing not found
518
        raise PricingBookingCheckTypeError(
519
            details={
520
                'check_type_group': check_type.group.slug,
521
                'check_type': check_type.slug,
522
                'reason': 'not-configured',
523
            }
524
        )
525

  
526
    def iter_pricing_matrix(self):
527
        categories = self.pricing.categories.all().order_by('pricingcriteriacategory__order')[:3]
528
        pricing_data = self.pricing_data or {}
529

  
530
        if not categories:
531
            return
532

  
533
        if len(categories) < 3:
534
            yield self.get_pricing_matrix(
535
                main_criteria=None, categories=categories, pricing_data=pricing_data
536
            )
537
            return
538

  
539
        # criterias are ordered
540
        for criteria in self.pricing.criterias.all():
541
            if criteria.category != categories[0]:
542
                continue
543
            yield self.get_pricing_matrix(
544
                main_criteria=criteria,
545
                categories=categories[1:],
546
                pricing_data=pricing_data.get(criteria.identifier) or {},
547
            )
548

  
549
    def get_pricing_matrix(self, main_criteria, categories, pricing_data):
550
        matrix = PricingMatrix(
551
            criteria=main_criteria,
552
            rows=[],
553
        )
554

  
555
        def get_pricing_matrix_cell(criteria_2, criteria_3, _pricing_data):
556
            try:
557
                value = decimal.Decimal(str(_pricing_data.get(criteria_3.identifier)))
558
            except (decimal.InvalidOperation, ValueError, TypeError):
559
                value = None
560
            return PricingMatrixCell(criteria=criteria_2, value=value)
561

  
562
        if len(categories) < 2:
563
            rows = [
564
                PricingMatrixRow(
565
                    criteria=criteria,
566
                    cells=[get_pricing_matrix_cell(None, criteria, pricing_data)],
567
                )
568
                for criteria in self.pricing.criterias.all()
569
                if criteria.category == categories[0]
570
            ]
571
            matrix.rows = rows
572
        else:
573
            criterias_2 = [c for c in self.pricing.criterias.all() if c.category == categories[0]]
574
            criterias_3 = [c for c in self.pricing.criterias.all() if c.category == categories[1]]
575

  
576
            rows = [
577
                PricingMatrixRow(
578
                    criteria=criteria_3,
579
                    cells=[
580
                        get_pricing_matrix_cell(
581
                            criteria_2,
582
                            criteria_3,
583
                            pricing_data.get(criteria_2.identifier) or {},
584
                        )
585
                        for criteria_2 in criterias_2
586
                    ],
587
                )
588
                for criteria_3 in criterias_3
589
            ]
590
            matrix.rows = rows
591

  
592
        return matrix
chrono/pricing/templates/chrono/pricing/manager_agenda_pricing_detail.html
1
{% extends "chrono/manager_agenda_settings.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'chrono-manager-agenda-pricing-detail' agenda.pk object.pk %}">{{ object.pricing }}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>
11
    {{ object.pricing }} ({{ object.date_start|date:'d/m/Y' }} - {{ object.date_end|date:'d/m/Y' }})
12
</h2>
13
{% if user_can_manage %}
14
<span class="actions">
15
  <a class="extra-actions-menu-opener"></a>
16
  <ul class="extra-actions-menu">
17
    <li><a rel="popup" href="{% url 'chrono-manager-agenda-pricing-edit' agenda.pk object.pk %}">{% trans 'Options' %}</a></li>
18
    <li><a rel="popup" href="{% url 'chrono-manager-agenda-pricing-delete' agenda.pk object.pk %}">{% trans 'Delete' %}</a></li>
19
  </ul>
20
</span>
21
{% endif %}
22
{% endblock %}
23

  
24
{% block content %}
25
{% for matrix in object.iter_pricing_matrix %}
26
<div class="section">
27
  {% if matrix.criteria %}<h3>{{ matrix.criteria.label }}</h3>{% endif %}
28
<div>
29
  <table class="main pricing-matrix-{{ matrix.criteria.slug }}">
30
    {% if matrix.rows.0.cells.0.criteria %}
31
    <thead>
32
      <tr>
33
        <th></th>
34
        {% for cell in matrix.rows.0.cells %}<th scope="col">{{ cell.criteria.label }}</th>{% endfor %}
35
      </tr>
36
    </thead>
37
    {% endif %}
38
    <tbody>
39
      {% for row in matrix.rows %}
40
        <tr class="pricing-row-{{ row.criteria.slug }}">
41
          <th scope="row">{{ row.criteria.label }}</th>
42
          {% for cell in row.cells %}<td class="pricing-cell-{{ cell.criteria.slug }}">{{ cell.value|floatformat:"2"|default_if_none:"" }}</td>{% endfor %}
43
        </tr>
44
      {% endfor %}
45
    </tbody>
46
  </table>
47
  {% if user_can_manage %}
48
  <p>
49
  <a class="pk-button" href="{% if matrix.criteria %}{% url 'chrono-manager-agenda-pricing-matrix-slug-edit' agenda.pk object.pk matrix.criteria.slug %}{% else %}{% url 'chrono-manager-agenda-pricing-matrix-edit' agenda.pk object.pk %}{% endif %}">{% trans "Edit pricing" %}</a>
50
  </p>
51
  {% endif %}
52
</div>
53
</div>
54
{% empty %}
55
<div class="big-msg-info">
56
  {% blocktrans %}
57
  This pricing model is misconfigured.
58
  {% endblocktrans %}
59
</div>
60
{% endfor %}
61
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_agenda_pricing_form.html
1
{% extends "chrono/manager_agenda_settings.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
{% if form.instance.pk %}
7
<a href="{% url 'chrono-manager-agenda-pricing-detail' agenda.pk object.pk %}">{{ object.pricing }}</a>
8
<a href="{% url 'chrono-manager-agenda-pricing-edit' agenda.pk object.pk %}">{% trans "Edit" %}</a>
9
{% else %}
10
<a href="{% url 'chrono-manager-agenda-pricing-add' agenda.pk %}">{% trans "New pricing" %}</a>
11
{% endif %}
12
{% endblock %}
13

  
14
{% block appbar %}
15
{% if object.pk %}
16
<h2>{% trans "Edit pricing" %}</h2>
17
{% else %}
18
<h2>{% trans "New pricing" %}</h2>
19
{% endif %}
20
{% endblock %}
21

  
22
{% block content %}
23
<form method="post" enctype="multipart/form-data">
24
  {% csrf_token %}
25
  {{ form.as_p }}
26
  <div class="buttons">
27
    <button class="submit-button">{% trans "Save" %}</button>
28
    {% if object.pk %}
29
    <a class="cancel" href="{% url 'chrono-manager-agenda-pricing-detail' agenda.pk object.pk %}">{% trans 'Cancel' %}</a>
30
    {% else %}
31
    <a class="cancel" href="{% url 'chrono-manager-agenda-settings' agenda.pk %}">{% trans 'Cancel' %}</a>
32
    {% endif %}
33
  </div>
34
</form>
35
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_agenda_pricing_matrix_form.html
1
{% extends "chrono/pricing/manager_agenda_pricing_detail.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
{% if matrix.criteria %}
7
<a href="{% url 'chrono-manager-agenda-pricing-matrix-slug-edit' agenda.pk object.pk matrix.criteria.slug %}">{% trans "Edit pricing" %}</a>
8
{% else %}
9
<a href="{% url 'chrono-manager-agenda-pricing-matrix-edit' agenda.pk object.pk %}">{% trans "Edit pricing" %}</a>
10
{% endif %}
11
{% endblock %}
12

  
13
{% block appbar %}
14
<h2>{% trans "Edit pricing" %}</h2>
15
{% endblock %}
16

  
17
{% block content %}
18
<div class="section">
19
  {% if matrix.criteria %}<h3>{{ matrix.criteria.label }}</h3>{% endif %}
20
<div>
21
<form method="post" enctype="multipart/form-data">
22
  {% csrf_token %}
23
  {{ form.management_form }}
24
  <table class="main">
25
    {% if matrix.rows.0.cells.0.criteria %}
26
    <thead>
27
      <tr>
28
        <th></th>
29
        {% for cell in matrix.rows.0.cells %}
30
        <th>{{ cell.criteria.label }}</th>
31
        {% endfor %}
32
      </tr>
33
    </thead>
34
    {% endif %}
35
    <tbody>
36
    {% for sub_form in form %}
37
    {% with row=matrix.rows|get:forloop.counter0 %}
38
    <tr>
39
      <th>{{ row.criteria.label }}</th>
40
      {% for field in sub_form %}
41
      <td>
42
        {{ field.errors.as_ul }}
43
        {{ field }}
44
      </td>
45
      {% endfor %}
46
    </tr>
47
    {% endwith %}
48
    {% endfor %}
49
    </tbody>
50
  </table>
51
  <div class="buttons">
52
    <button class="submit-button">{% trans "Save" %}</button>
53
    <a class="cancel" href="{% url 'chrono-manager-agenda-pricing-detail' agenda.pk object.pk %}">{% trans 'Cancel' %}</a>
54
  </div>
55
</form>
56
</div>
57
</div>
58
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_criteria_category_form.html
1
{% extends "chrono/pricing/manager_criteria_list.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
{% if object.pk %}
7
<a href="{% url 'chrono-manager-pricing-criteria-category-edit' object.pk %}">{{ object }}</a>
8
{% else %}
9
<a href="{% url 'chrono-manager-pricing-criteria-category-add' %}">{% trans "New category" %}</a>
10
{% endif %}
11
{% endblock %}
12

  
13
{% block appbar %}
14
{% if object.pk %}
15
<h2>{% trans "Edit category" %}</h2>
16
{% else %}
17
<h2>{% trans "New category" %}</h2>
18
{% endif %}
19
{% endblock %}
20

  
21
{% block content %}
22

  
23
<form method="post" enctype="multipart/form-data">
24
  {% csrf_token %}
25
  {{ form.as_p }}
26
  <div class="buttons">
27
    <button class="submit-button">{% trans "Save" %}</button>
28
    <a class="cancel" href="{% url 'chrono-manager-pricing-criteria-list' %}">{% trans 'Cancel' %}</a>
29
  </div>
30
</form>
31
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_criteria_form.html
1
{% extends "chrono/pricing/manager_criteria_list.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
{% if form.instance.pk %}
7
<a href="{% url 'chrono-manager-pricing-criteria-edit' form.instance.category_id form.instance.pk %}">{{ form.instance }}</a>
8
{% else %}
9
<a href="{% url 'chrono-manager-pricing-criteria-add' form.instance.category_id %}">{% trans "New criteria" %}</a>
10
{% endif %}
11
{% endblock %}
12

  
13
{% block appbar %}
14
{% if form.instance.pk %}
15
<h2>{{ form.instance.category }} - {% trans "Edit criteria" %}</h2>
16
{% else %}
17
<h2>{{ form.instance.category }} - {% trans "New criteria" %}</h2>
18
{% endif %}
19
{% endblock %}
20

  
21
{% block content %}
22

  
23
<form method="post" enctype="multipart/form-data">
24
  {% csrf_token %}
25
  {{ form.as_p }}
26
  <div class="buttons">
27
    <button class="submit-button">{% trans "Save" %}</button>
28
    <a class="cancel" href="{% url 'chrono-manager-pricing-criteria-list' %}">{% trans 'Cancel' %}</a>
29
  </div>
30
</form>
31
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_criteria_list.html
1
{% extends "chrono/pricing/manager_pricing_list.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'chrono-manager-pricing-criteria-list' %}">{% trans "Criterias" %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans 'Criterias' %}</h2>
11
<span class="actions">
12
<a rel="popup" href="{% url 'chrono-manager-pricing-criteria-category-add' %}">{% trans 'New category' %}</a>
13
</span>
14
{% endblock %}
15

  
16

  
17
{% block content %}
18
<div class="pk-information">
19
<p>{% trans "Define here pricing criterias used in pricing models." %}</p>
20
</div>
21
{% if object_list %}
22
<p class="hint">
23
{% blocktrans %}
24
Use drag and drop with the ⣿ handles to reorder criterias inside a category.
25
{% endblocktrans %}
26
</p>
27
{% endif %}
28
{% for object in object_list %}
29
<div class="section criteria-category">
30
  <h3>
31
    <a rel="popup" href="{% url 'chrono-manager-pricing-criteria-category-edit' object.pk %}">{{ object }} [{{ object.slug }}]</a>
32
    <span>
33
    <a class="button" href="{% url 'chrono-manager-pricing-criteria-category-export' object.pk %}">{% trans "Export"%}</a>
34
    <a class="button" rel="popup" href="{% url 'chrono-manager-pricing-criteria-category-delete' object.pk %}">{% trans "Delete"%}</a>
35
    </span>
36
  </h3>
37
  <div>
38
  <ul class="objects-list single-links sortable" data-order-url="{% url 'chrono-manager-pricing-criteria-order' object.pk %}">
39
    {% for criteria in object.criterias.all %}
40
    <li class="sortable-item" data-item-id="{{ criteria.pk }}">
41
        <span class="handle">⣿</span>
42
        <a rel="popup" href="{% url 'chrono-manager-pricing-criteria-edit' object.pk criteria.pk %}">{{ criteria }}</a>
43
        <a class="delete" rel="popup" href="{% url 'chrono-manager-pricing-criteria-delete' object.pk criteria.pk %}">{% trans "delete"%}</a>
44
    </li>
45
    {% endfor %}
46
    <li><a class="add" rel="popup" href="{% url 'chrono-manager-pricing-criteria-add' object.pk %}">{% trans "Add a criteria" %}</a></li>
47
  </ul>
48
  </div>
49
</div>
50
{% empty %}
51
<div class="big-msg-info">
52
  {% blocktrans %}
53
  This site doesn't have any pricing category yet. Click on the "New category" button in the top
54
  right of the page to add a first one.
55
  {% endblocktrans %}
56
</div>
57
{% endfor %}
58
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_pricing_criteria_category_form.html
1
{% extends "chrono/pricing/manager_pricing_list.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'chrono-manager-pricing-detail' object.pk %}">{{ object }}</a>
7
{% if category %}
8
<a href="{% url 'chrono-manager-pricing-criteria-category-edit' object.pk category.pk %}">{% trans "Select criterias" %}</a>
9
{% else %}
10
<a href="{% url 'chrono-manager-pricing-criteria-category-add' object.pk %}">{% trans "Add a category" %}</a>
11
{% endif %}
12
{% endblock %}
13

  
14
{% block appbar %}
15
{% if category %}
16
<h2>{% trans "Select criterias" %}</h2>
17
{% else %}
18
<h2>{% trans "Add a category" %}</h2>
19
{% endif %}
20
{% endblock %}
21

  
22
{% block content %}
23
<form method="post" enctype="multipart/form-data">
24
  {% csrf_token %}
25
  {{ form.as_p }}
26
  <div class="buttons">
27
    <button class="submit-button">{% trans "Save" %}</button>
28
    <a class="cancel" href="{% url 'chrono-manager-pricing-detail' object.pk %}">{% trans 'Cancel' %}</a>
29
  </div>
30
</form>
31
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_pricing_detail.html
1
{% extends "chrono/pricing/manager_pricing_list.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'chrono-manager-pricing-detail' object.pk %}">{{ object }}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>
11
    {{ object }}
12
    <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]</span>
13
</h2>
14
<span class="actions">
15
  <a class="extra-actions-menu-opener"></a>
16
  <ul class="extra-actions-menu">
17
    <li><a rel="popup" href="{% url 'chrono-manager-pricing-edit' pk=object.pk %}">{% trans 'Options' %}</a></li>
18
    <li><a rel="popup" class="action-duplicate" href="{% url 'chrono-manager-pricing-duplicate' pk=object.pk %}">{% trans 'Duplicate' %}</a></li>
19
    <li><a href="{% url 'chrono-manager-pricing-export' pk=object.pk %}">{% trans 'Export' %}</a></li>
20
    <li><a rel="popup" href="{% url 'chrono-manager-pricing-delete' pk=object.pk %}">{% trans 'Delete' %}</a></li>
21
  </ul>
22
</span>
23
{% endblock %}
24

  
25
{% block content %}
26
<div class="section">
27
  <h3>{% trans "Variables" %}</h3>
28

  
29
  <div>
30
    {% if object.extra_variables %}
31
    <p>
32
    <label>{% trans 'Extra variables:' %}</label>
33
    {% for key in object.get_extra_variables_keys %}<i>{{ key }}</i>{% if not forloop.last %}, {% endif %}{% endfor %}
34
    </p>
35
    {% endif %}
36
    <a class="pk-button" rel="popup" href="{% url 'chrono-manager-pricing-variable-edit' pk=object.pk %}">{% trans 'Define variables' %}</a>
37
  </div>
38
</div>
39

  
40
<div class="section">
41
  <h3>{% trans "Criterias" %}</h3>
42
  {% with criterias=object.criterias.all categories=object.categories.all %}
43
  {% if categories %}
44
  <div>
45
  {% blocktrans %}Use drag and drop with the ⣿ handles to reorder categories.{% endblocktrans %}
46
  </div>
47
  {% endif %}
48
  <div class="sortable" data-order-url="{% url 'chrono-manager-pricing-criteria-category-order' object.pk %}">
49
    {% for category in categories %}
50
    <div class="paragraph sortable-item" data-item-id="{{ category.pk }}">
51
      <h4><span class="handle">⣿</span>{% trans "Category:" %} {{ category }}</h4>
52
      <p>{% trans "Selected criterias:" %}</p>
53
      <ul>
54
        {% for criteria in criterias %}
55
        {% if criteria.category == category %}
56
        <li>{{ criteria }}</li>
57
        {% endif %}
58
        {% endfor %}
59
      </ul>
60
      <p>
61
        <a rel="popup" class="pk-button" href="{% url 'chrono-manager-pricing-criteria-category-edit' pk=object.pk category_pk=category.pk %}">{% trans "Change criterias selection" %}</a>
62
        <a rel="popup" class="pk-button" href="{% url 'chrono-manager-pricing-criteria-category-delete' pk=object.pk category_pk=category.pk %}">{% trans "Remove this category" %}</a>
63
      </p>
64
    </div>
65
    {% endfor %}
66
    {% if object.categories.count < 3 %}
67
    <p>
68
      <a rel="popup" class="pk-button" href="{% url 'chrono-manager-pricing-criteria-category-add' pk=object.pk %}">{% trans "Add a category" %} <span class="extra-info">({% trans "max 3 categories" %})</span></a>
69
    </p>
70
    {% endif %}
71
  {% endwith %}
72
  </div>
73
</div>
74
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_pricing_duplicate_form.html
1
{% extends "chrono/pricing/manager_pricing_detail.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'chrono-manager-pricing-duplicate' object.pk %}">{% trans "Duplicate pricing model" %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans "Duplicate pricing model" %}</h2>
11
{% endblock %}
12

  
13
{% block content %}
14
<form method="post" enctype="multipart/form-data">
15
  {% csrf_token %}
16
  {{ form.as_p }}
17
  <div class="buttons">
18
    <button class="submit-button">{% trans "Duplicate" %}</button>
19
    <a class="cancel" href="{% url 'chrono-manager-pricing-detail' object.pk %}">{% trans 'Cancel' %}</a>
20
  </div>
21
</form>
22
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_pricing_form.html
1
{% extends "chrono/pricing/manager_pricing_list.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
{% if object.pk %}
7
<a href="{% url 'chrono-manager-pricing-detail' object.pk %}">{{ object }}</a>
8
<a href="{% url 'chrono-manager-pricing-edit' object.pk %}">{% trans "Edit" %}</a>
9
{% else %}
10
<a href="{% url 'chrono-manager-pricing-add' %}">{% trans "New pricing model" %}</a>
11
{% endif %}
12
{% endblock %}
13

  
14
{% block appbar %}
15
{% if object.pk %}
16
<h2>{% trans "Edit pricing model" %}</h2>
17
{% else %}
18
<h2>{% trans "New pricing model" %}</h2>
19
{% endif %}
20
{% endblock %}
21

  
22
{% block content %}
23
<form method="post" enctype="multipart/form-data">
24
  {% csrf_token %}
25
  {{ form.as_p }}
26
  <div class="buttons">
27
    <button class="submit-button">{% trans "Save" %}</button>
28
    <a class="cancel" href="{% url 'chrono-manager-pricing-list' %}">{% trans 'Cancel' %}</a>
29
  </div>
30
</form>
31
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_pricing_list.html
1
{% extends "chrono/manager_base.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'chrono-manager-pricing-list' %}">{% trans "Pricing" context 'pricing' %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans 'Pricing' context 'pricing' %}</h2>
11
<span class="actions">
12
<a href="{% url 'chrono-manager-pricing-criteria-list' %}">{% trans 'Criterias' %}</a>
13
<a rel="popup" href="{% url 'chrono-manager-pricing-add' %}">{% trans 'New pricing model' %}</a>
14
</span>
15
{% endblock %}
16

  
17
{% block content %}
18
<div class="pk-information">
19
<p>{% trans "Define here pricing models used in events agendas." %}</p>
20
</div>
21
{% if object_list %}
22
<div>
23
  <ul class="objects-list single-links">
24
    {% for object in object_list %}
25
    <li>
26
        <a href="{% url 'chrono-manager-pricing-detail' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
27
    </li>
28
    {% endfor %}
29
  </ul>
30
</div>
31
{% else %}
32
<div class="big-msg-info">
33
  {% blocktrans %}
34
  This site doesn't have any pricing model yet. Click on the "New" button in the top
35
  right of the page to add a first one.
36
  {% endblocktrans %}
37
</div>
38
{% endif %}
39
{% endblock %}
chrono/pricing/templates/chrono/pricing/manager_pricing_variable_form.html
1
{% extends "chrono/pricing/manager_pricing_detail.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'chrono-manager-pricing-variable-edit' object.pk %}">{% trans "Variable definition" %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans "Variable definition" %}</h2>
11
{% endblock %}
12

  
13
{% block content %}
14
<form method="post" enctype="multipart/form-data">
15
  {% csrf_token %}
16
  {{ form.management_form }}
17
  <table id="pricing-variable-forms">
18
    <thead>
19
      <tr>
20
        {% for field in form.0 %}
21
        <th class="column-{{ field.name }}{% if field.required %} required{% endif %}">{{ field.label }}</th>
22
        {% endfor %}
23
      </tr>
24
    </thead>
25
    <tbody>
26
    {% for sub_form in form %}
27
    <tr class='pricing-variable-form'>
28
      {% for field in sub_form %}
29
      <td class="field-{{ field.name }}">
30
        {{ field.errors.as_ul }}
31
        {{ field }}
32
      </td>
33
      {% endfor %}
34
    </tr>
35
    {% endfor %}
36
    </tbody>
37
  </table>
38
  <button id="add-pricing-variable-form" type="button">{% trans "Add another variable" %}</button>
39
  <div class="buttons">
40
    <button class="submit-button">{% trans "Save" %}</button>
41
    <a class="cancel" href="{% url 'chrono-manager-pricing-detail' object.pk %}">{% trans 'Cancel' %}</a>
42
  </div>
43
</form>
44
{% endblock %}
chrono/pricing/urls.py
1
# chrono - agendas system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18

  
19
from . import views
20

  
21
urlpatterns = [
22
    url(r'^$', views.pricing_list, name='chrono-manager-pricing-list'),
23
    url(
24
        r'^add/$',
25
        views.pricing_add,
26
        name='chrono-manager-pricing-add',
27
    ),
28
    url(
29
        r'^(?P<pk>\d+)/$',
30
        views.pricing_detail,
31
        name='chrono-manager-pricing-detail',
32
    ),
33
    url(
34
        r'^(?P<pk>\d+)/edit/$',
35
        views.pricing_edit,
36
        name='chrono-manager-pricing-edit',
37
    ),
38
    url(
39
        r'^(?P<pk>\d+)/delete/$',
40
        views.pricing_delete,
41
        name='chrono-manager-pricing-delete',
42
    ),
43
    url(
44
        r'^(?P<pk>\d+)/duplicate/$',
45
        views.pricing_duplicate,
46
        name='chrono-manager-pricing-duplicate',
47
    ),
48
    url(
49
        r'^(?P<pk>\d+)/export/$',
50
        views.pricing_export,
51
        name='chrono-manager-pricing-export',
52
    ),
53
    url(
54
        r'^(?P<pk>\d+)/variable/$',
55
        views.pricing_variable_edit,
56
        name='chrono-manager-pricing-variable-edit',
57
    ),
58
    url(
59
        r'^(?P<pk>\d+)/category/add/$',
60
        views.pricing_criteria_category_add,
61
        name='chrono-manager-pricing-criteria-category-add',
62
    ),
63
    url(
64
        r'^(?P<pk>\d+)/category/(?P<category_pk>\d+)/edit/$',
65
        views.pricing_criteria_category_edit,
66
        name='chrono-manager-pricing-criteria-category-edit',
67
    ),
68
    url(
69
        r'^(?P<pk>\d+)/category/(?P<category_pk>\d+)/delete/$',
70
        views.pricing_criteria_category_delete,
71
        name='chrono-manager-pricing-criteria-category-delete',
72
    ),
73
    url(
74
        r'^(?P<pk>\d+)/order/$',
75
        views.pricing_criteria_category_order,
76
        name='chrono-manager-pricing-criteria-category-order',
77
    ),
78
    url(r'^criterias/$', views.criteria_list, name='chrono-manager-pricing-criteria-list'),
79
    url(
80
        r'^criteria/category/add/$',
81
        views.criteria_category_add,
82
        name='chrono-manager-pricing-criteria-category-add',
83
    ),
84
    url(
85
        r'^criteria/category/(?P<pk>\d+)/edit/$',
86
        views.criteria_category_edit,
87
        name='chrono-manager-pricing-criteria-category-edit',
88
    ),
89
    url(
90
        r'^criteria/category/(?P<pk>\d+)/delete/$',
91
        views.criteria_category_delete,
92
        name='chrono-manager-pricing-criteria-category-delete',
93
    ),
94
    url(
95
        r'^criteria/category/(?P<pk>\d+)/export/$',
96
        views.criteria_category_export,
97
        name='chrono-manager-pricing-criteria-category-export',
98
    ),
99
    url(
100
        r'^criteria/category/(?P<pk>\d+)/order/$',
101
        views.criteria_order,
102
        name='chrono-manager-pricing-criteria-order',
103
    ),
104
    url(
105
        r'^criteria/category/(?P<category_pk>\d+)/add/$',
106
        views.criteria_add,
107
        name='chrono-manager-pricing-criteria-add',
108
    ),
109
    url(
110
        r'^criteria/category/(?P<category_pk>\d+)/(?P<pk>\d+)/edit/$',
111
        views.criteria_edit,
112
        name='chrono-manager-pricing-criteria-edit',
113
    ),
114
    url(
115
        r'^criteria/category/(?P<category_pk>\d+)/(?P<pk>\d+)/delete/$',
116
        views.criteria_delete,
117
        name='chrono-manager-pricing-criteria-delete',
118
    ),
119
    url(
120
        r'^agenda/(?P<pk>\d+)/pricing/add/$',
121
        views.agenda_pricing_add,
122
        name='chrono-manager-agenda-pricing-add',
123
    ),
124
    url(
125
        r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/$',
126
        views.agenda_pricing_detail,
127
        name='chrono-manager-agenda-pricing-detail',
128
    ),
129
    url(
130
        r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/edit/$',
131
        views.agenda_pricing_edit,
132
        name='chrono-manager-agenda-pricing-edit',
133
    ),
134
    url(
135
        r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/delete/$',
136
        views.agenda_pricing_delete,
137
        name='chrono-manager-agenda-pricing-delete',
138
    ),
139
    url(
140
        r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/matrix/edit/$',
141
        views.agenda_pricing_matrix_edit,
142
        name='chrono-manager-agenda-pricing-matrix-edit',
143
    ),
144
    url(
145
        r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/matrix/(?P<slug>[-_a-zA-Z0-9]+)/edit/$',
146
        views.agenda_pricing_matrix_edit,
147
        name='chrono-manager-agenda-pricing-matrix-slug-edit',
148
    ),
149
]
chrono/pricing/views.py
1
# chrono - agendas system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import datetime
18
import json
19
from collections import defaultdict
20
from operator import itemgetter
21

  
22
from django import forms
23
from django.core.exceptions import PermissionDenied
24
from django.db.models import Prefetch
25
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
26
from django.shortcuts import get_object_or_404
27
from django.urls import reverse
28
from django.views.generic import CreateView, DeleteView, DetailView, FormView, ListView, UpdateView
29
from django.views.generic.detail import SingleObjectMixin
30

  
31
from chrono.agendas.models import Agenda
32
from chrono.manager.views import ManagedAgendaMixin, ViewableAgendaMixin
33
from chrono.pricing.forms import (
34
    AgendaPricingForm,
35
    CriteriaForm,
36
    NewCriteriaForm,
37
    PricingCriteriaCategoryAddForm,
38
    PricingCriteriaCategoryEditForm,
39
    PricingDuplicateForm,
40
    PricingMatrixForm,
41
    PricingVariableFormSet,
42
)
43
from chrono.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
44

  
45

  
46
class PricingListView(ListView):
47
    template_name = 'chrono/pricing/manager_pricing_list.html'
48
    model = Pricing
49

  
50
    def dispatch(self, request, *args, **kwargs):
51
        if not request.user.is_staff:
52
            raise PermissionDenied()
53
        return super().dispatch(request, *args, **kwargs)
54

  
55

  
56
pricing_list = PricingListView.as_view()
57

  
58

  
59
class CriteriaListView(ListView):
60
    template_name = 'chrono/pricing/manager_criteria_list.html'
61
    model = CriteriaCategory
62

  
63
    def dispatch(self, request, *args, **kwargs):
64
        if not request.user.is_staff:
65
            raise PermissionDenied()
66
        return super().dispatch(request, *args, **kwargs)
67

  
68
    def get_queryset(self):
69
        return CriteriaCategory.objects.prefetch_related('criterias')
70

  
71

  
72
criteria_list = CriteriaListView.as_view()
73

  
74

  
75
class PricingAddView(CreateView):
76
    template_name = 'chrono/pricing/manager_pricing_form.html'
77
    model = Pricing
78
    fields = ['label']
79

  
80
    def dispatch(self, request, *args, **kwargs):
81
        if not request.user.is_staff:
82
            raise PermissionDenied()
83
        return super().dispatch(request, *args, **kwargs)
84

  
85
    def get_success_url(self):
86
        return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
87

  
88

  
89
pricing_add = PricingAddView.as_view()
90

  
91

  
92
class PricingDetailView(DetailView):
93
    template_name = 'chrono/pricing/manager_pricing_detail.html'
94
    model = Pricing
95

  
96
    def dispatch(self, request, *args, **kwargs):
97
        if not request.user.is_staff:
98
            raise PermissionDenied()
99
        return super().dispatch(request, *args, **kwargs)
100

  
101
    def get_queryset(self):
102
        return (
103
            super()
104
            .get_queryset()
105
            .prefetch_related(
106
                Prefetch(
107
                    'categories', queryset=CriteriaCategory.objects.order_by('pricingcriteriacategory__order')
108
                )
109
            )
110
        )
111

  
112

  
113
pricing_detail = PricingDetailView.as_view()
114

  
115

  
116
class PricingEditView(UpdateView):
117
    template_name = 'chrono/pricing/manager_pricing_form.html'
118
    model = Pricing
119
    fields = ['label', 'slug']
120

  
121
    def dispatch(self, request, *args, **kwargs):
122
        if not request.user.is_staff:
123
            raise PermissionDenied()
124
        return super().dispatch(request, *args, **kwargs)
125

  
126
    def get_success_url(self):
127
        return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
128

  
129

  
130
pricing_edit = PricingEditView.as_view()
131

  
132

  
133
class PricingDeleteView(DeleteView):
134
    template_name = 'chrono/manager_confirm_delete.html'
135
    model = Pricing
136

  
137
    def dispatch(self, request, *args, **kwargs):
138
        if not request.user.is_staff:
139
            raise PermissionDenied()
140
        return super().dispatch(request, *args, **kwargs)
141

  
142
    def get_success_url(self):
143
        return reverse('chrono-manager-pricing-list')
144

  
145

  
146
pricing_delete = PricingDeleteView.as_view()
147

  
148

  
149
class PricingDuplicate(SingleObjectMixin, FormView):
150
    template_name = 'chrono/pricing/manager_pricing_duplicate_form.html'
151
    model = Pricing
152
    form_class = PricingDuplicateForm
153

  
154
    def dispatch(self, request, *args, **kwargs):
155
        self.object = self.get_object()
156
        if not request.user.is_staff:
157
            raise PermissionDenied()
158
        return super().dispatch(request, *args, **kwargs)
159

  
160
    def get_success_url(self):
161
        return reverse('chrono-manager-pricing-detail', kwargs={'pk': self.new_pricing.pk})
162

  
163
    def form_valid(self, form):
164
        self.new_pricing = self.object.duplicate(label=form.cleaned_data['label'])
165
        return super().form_valid(form)
166

  
167

  
168
pricing_duplicate = PricingDuplicate.as_view()
169

  
170

  
171
class PricingExport(DetailView):
172
    model = Pricing
173

  
174
    def dispatch(self, request, *args, **kwargs):
175
        if not request.user.is_staff:
176
            raise PermissionDenied()
177
        return super().dispatch(request, *args, **kwargs)
178

  
179
    def get(self, request, *args, **kwargs):
180
        response = HttpResponse(content_type='application/json')
181
        today = datetime.date.today()
182
        attachment = 'attachment; filename="export_pricing_{}_{}.json"'.format(
183
            self.get_object().slug, today.strftime('%Y%m%d')
184
        )
185
        response['Content-Disposition'] = attachment
186
        json.dump({'pricing_models': [self.get_object().export_json()]}, response, indent=2)
187
        return response
188

  
189

  
190
pricing_export = PricingExport.as_view()
191

  
192

  
193
class PricingVariableEdit(FormView):
194
    template_name = 'chrono/pricing/manager_pricing_variable_form.html'
195
    model = Pricing
196
    form_class = PricingVariableFormSet
197

  
198
    def dispatch(self, request, *args, **kwargs):
199
        self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
200
        if not request.user.is_staff:
201
            raise PermissionDenied()
202
        return super().dispatch(request, *args, **kwargs)
203

  
204
    def get_context_data(self, **kwargs):
205
        kwargs['object'] = self.object
206
        return super().get_context_data(**kwargs)
207

  
208
    def get_initial(self):
209
        return sorted(
210
            ({'key': k, 'value': v} for k, v in self.object.extra_variables.items()),
211
            key=itemgetter('key'),
212
        )
213

  
214
    def form_valid(self, form):
215
        self.object.extra_variables = {}
216
        for sub_data in form.cleaned_data:
217
            if not sub_data.get('key'):
218
                continue
219
            self.object.extra_variables[sub_data['key']] = sub_data['value']
220
        self.object.save()
221
        return HttpResponseRedirect(self.get_success_url())
222

  
223
    def get_success_url(self):
224
        return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
225

  
226

  
227
pricing_variable_edit = PricingVariableEdit.as_view()
228

  
229

  
230
class PricingCriteriaCategoryAddView(FormView):
231
    template_name = 'chrono/pricing/manager_pricing_criteria_category_form.html'
232
    model = Pricing
233
    form_class = PricingCriteriaCategoryAddForm
234

  
235
    def dispatch(self, request, *args, **kwargs):
236
        self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
237
        if self.object.categories.count() >= 3:
238
            raise Http404
239
        if not request.user.is_staff:
240
            raise PermissionDenied()
241
        return super().dispatch(request, *args, **kwargs)
242

  
243
    def get_form_kwargs(self):
244
        kwargs = super().get_form_kwargs()
245
        kwargs['pricing'] = self.object
246
        return kwargs
247

  
248
    def get_context_data(self, **kwargs):
249
        kwargs['object'] = self.object
250
        return super().get_context_data(**kwargs)
251

  
252
    def form_valid(self, form):
253
        PricingCriteriaCategory.objects.create(pricing=self.object, category=form.cleaned_data['category'])
254
        return super().form_valid(form)
255

  
256
    def get_success_url(self):
257
        return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
258

  
259

  
260
pricing_criteria_category_add = PricingCriteriaCategoryAddView.as_view()
261

  
262

  
263
class PricingCriteriaCategoryEditView(FormView):
264
    template_name = 'chrono/pricing/manager_pricing_criteria_category_form.html'
265
    model = Pricing
266
    form_class = PricingCriteriaCategoryEditForm
267

  
268
    def dispatch(self, request, *args, **kwargs):
269
        self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
270
        self.category = get_object_or_404(self.object.categories, pk=kwargs['category_pk'])
271
        if not request.user.is_staff:
272
            raise PermissionDenied()
273
        return super().dispatch(request, *args, **kwargs)
274

  
275
    def get_form_kwargs(self):
276
        kwargs = super().get_form_kwargs()
277
        kwargs['pricing'] = self.object
278
        kwargs['category'] = self.category
279
        return kwargs
280

  
281
    def get_context_data(self, **kwargs):
282
        kwargs['object'] = self.object
283
        kwargs['category'] = self.category
284
        return super().get_context_data(**kwargs)
285

  
286
    def form_valid(self, form):
287
        old_criterias = self.object.criterias.filter(category=self.category)
288
        new_criterias = form.cleaned_data['criterias']
289
        removed_criterias = set(old_criterias) - set(new_criterias)
290
        self.object.criterias.remove(*removed_criterias)
291
        self.object.criterias.add(*new_criterias)
292
        return super().form_valid(form)
293

  
294
    def get_success_url(self):
295
        return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
296

  
297

  
298
pricing_criteria_category_edit = PricingCriteriaCategoryEditView.as_view()
299

  
300

  
301
class PricingCriteriaCategoryDeleteView(DeleteView):
302
    template_name = 'chrono/manager_confirm_delete.html'
303
    model = CriteriaCategory
304
    pk_url_kwarg = 'category_pk'
305

  
306
    def dispatch(self, request, *args, **kwargs):
307
        self.pricing = get_object_or_404(Pricing, pk=kwargs['pk'])
308
        if not request.user.is_staff:
309
            raise PermissionDenied()
310
        return super().dispatch(request, *args, **kwargs)
311

  
312
    def get_queryset(self):
313
        return self.pricing.categories.all()
314

  
315
    def delete(self, request, *args, **kwargs):
316
        self.object = self.get_object()
317
        self.pricing.categories.remove(self.object)
318
        self.pricing.criterias.remove(*self.pricing.criterias.filter(category=self.object))
319
        return HttpResponseRedirect(self.get_success_url())
320

  
321
    def get_success_url(self):
322
        return reverse('chrono-manager-pricing-detail', args=[self.pricing.pk])
323

  
324

  
325
pricing_criteria_category_delete = PricingCriteriaCategoryDeleteView.as_view()
326

  
327

  
328
class PricingCriteriaCategoryOrder(DetailView):
329
    model = Pricing
330

  
331
    def dispatch(self, request, *args, **kwargs):
332
        if not request.user.is_staff:
333
            raise PermissionDenied()
334
        return super().dispatch(request, *args, **kwargs)
335

  
336
    def get(self, request, *args, **kwargs):
337
        if 'new-order' not in request.GET:
338
            return HttpResponseBadRequest('missing new-order parameter')
339
        pricing = self.get_object()
340
        try:
341
            new_order = [int(x) for x in request.GET['new-order'].split(',')]
342
        except ValueError:
343
            return HttpResponseBadRequest('incorrect new-order parameter')
344
        categories = pricing.categories.all()
345
        if set(new_order) != {x.pk for x in categories} or len(new_order) != len(categories):
346
            return HttpResponseBadRequest('incorrect new-order parameter')
347
        for i, c_id in enumerate(new_order):
348
            PricingCriteriaCategory.objects.filter(pricing=pricing, category=c_id).update(order=i + 1)
349
        return HttpResponse(status=204)
350

  
351

  
352
pricing_criteria_category_order = PricingCriteriaCategoryOrder.as_view()
353

  
354

  
355
class CriteriaCategoryAddView(CreateView):
356
    template_name = 'chrono/pricing/manager_criteria_category_form.html'
357
    model = CriteriaCategory
358
    fields = ['label']
359

  
360
    def dispatch(self, request, *args, **kwargs):
361
        if not request.user.is_staff:
362
            raise PermissionDenied()
363
        return super().dispatch(request, *args, **kwargs)
364

  
365
    def get_success_url(self):
366
        return reverse('chrono-manager-pricing-criteria-list')
367

  
368

  
369
criteria_category_add = CriteriaCategoryAddView.as_view()
370

  
371

  
372
class CriteriaCategoryEditView(UpdateView):
373
    template_name = 'chrono/pricing/manager_criteria_category_form.html'
374
    model = CriteriaCategory
375
    fields = ['label', 'slug']
376

  
377
    def dispatch(self, request, *args, **kwargs):
378
        if not request.user.is_staff:
379
            raise PermissionDenied()
380
        return super().dispatch(request, *args, **kwargs)
381

  
382
    def get_success_url(self):
383
        return reverse('chrono-manager-pricing-criteria-list')
384

  
385

  
386
criteria_category_edit = CriteriaCategoryEditView.as_view()
387

  
388

  
389
class CriteriaCategoryDeleteView(DeleteView):
390
    template_name = 'chrono/manager_confirm_delete.html'
391
    model = CriteriaCategory
392

  
393
    def dispatch(self, request, *args, **kwargs):
394
        if not request.user.is_staff:
395
            raise PermissionDenied()
396
        return super().dispatch(request, *args, **kwargs)
397

  
398
    def get_success_url(self):
399
        return reverse('chrono-manager-pricing-criteria-list')
400

  
401

  
402
criteria_category_delete = CriteriaCategoryDeleteView.as_view()
403

  
404

  
405
class CriteriaCategoryExport(DetailView):
406
    model = CriteriaCategory
407

  
408
    def dispatch(self, request, *args, **kwargs):
409
        if not request.user.is_staff:
410
            raise PermissionDenied()
411
        return super().dispatch(request, *args, **kwargs)
412

  
413
    def get(self, request, *args, **kwargs):
414
        response = HttpResponse(content_type='application/json')
415
        today = datetime.date.today()
416
        attachment = 'attachment; filename="export_pricing_category_{}_{}.json"'.format(
417
            self.get_object().slug, today.strftime('%Y%m%d')
418
        )
419
        response['Content-Disposition'] = attachment
420
        json.dump({'pricing_categories': [self.get_object().export_json()]}, response, indent=2)
421
        return response
422

  
423

  
424
criteria_category_export = CriteriaCategoryExport.as_view()
425

  
426

  
427
class CriteriaOrder(DetailView):
428
    model = CriteriaCategory
429

  
430
    def dispatch(self, request, *args, **kwargs):
431
        if not request.user.is_staff:
432
            raise PermissionDenied()
433
        return super().dispatch(request, *args, **kwargs)
434

  
435
    def get(self, request, *args, **kwargs):
436
        if 'new-order' not in request.GET:
437
            return HttpResponseBadRequest('missing new-order parameter')
438
        category = self.get_object()
439
        try:
440
            new_order = [int(x) for x in request.GET['new-order'].split(',')]
441
        except ValueError:
442
            return HttpResponseBadRequest('incorrect new-order parameter')
443
        criterias = category.criterias.all()
444
        if set(new_order) != {x.pk for x in criterias} or len(new_order) != len(criterias):
445
            return HttpResponseBadRequest('incorrect new-order parameter')
446
        criterias_by_id = {c.pk: c for c in criterias}
447
        for i, c_id in enumerate(new_order):
448
            criterias_by_id[c_id].order = i + 1
449
            criterias_by_id[c_id].save()
450
        return HttpResponse(status=204)
451

  
452

  
453
criteria_order = CriteriaOrder.as_view()
454

  
455

  
456
class CriteriaAddView(CreateView):
457
    template_name = 'chrono/pricing/manager_criteria_form.html'
458
    model = Criteria
459
    form_class = NewCriteriaForm
460

  
461
    def dispatch(self, request, *args, **kwargs):
462
        self.category_pk = kwargs.pop('category_pk')
463
        if not request.user.is_staff:
464
            raise PermissionDenied()
465
        return super().dispatch(request, *args, **kwargs)
466

  
467
    def get_form_kwargs(self):
468
        kwargs = super().get_form_kwargs()
469
        if not kwargs.get('instance'):
470
            kwargs['instance'] = self.model()
471
        kwargs['instance'].category_id = self.category_pk
472
        return kwargs
473

  
474
    def get_success_url(self):
475
        return reverse('chrono-manager-pricing-criteria-list')
476

  
477

  
478
criteria_add = CriteriaAddView.as_view()
479

  
480

  
481
class CriteriaEditView(UpdateView):
482
    template_name = 'chrono/pricing/manager_criteria_form.html'
483
    model = Criteria
484
    form_class = CriteriaForm
485

  
486
    def dispatch(self, request, *args, **kwargs):
487
        self.category_pk = kwargs.pop('category_pk')
488
        if not request.user.is_staff:
489
            raise PermissionDenied()
490
        return super().dispatch(request, *args, **kwargs)
491

  
492
    def get_queryset(self):
493
        return Criteria.objects.filter(category=self.category_pk)
494

  
495
    def get_success_url(self):
496
        return reverse('chrono-manager-pricing-criteria-list')
497

  
498

  
499
criteria_edit = CriteriaEditView.as_view()
500

  
501

  
502
class CriteriaDeleteView(DeleteView):
503
    template_name = 'chrono/manager_confirm_delete.html'
504
    model = Criteria
505

  
506
    def dispatch(self, request, *args, **kwargs):
507
        self.category_pk = kwargs.pop('category_pk')
508
        if not request.user.is_staff:
509
            raise PermissionDenied()
510
        return super().dispatch(request, *args, **kwargs)
511

  
512
    def get_queryset(self):
513
        return Criteria.objects.filter(category=self.category_pk)
514

  
515
    def get_success_url(self):
516
        return reverse('chrono-manager-pricing-criteria-list')
517

  
518

  
519
criteria_delete = CriteriaDeleteView.as_view()
520

  
521

  
522
class AgendaPricingAddView(ManagedAgendaMixin, CreateView):
523
    template_name = 'chrono/pricing/manager_agenda_pricing_form.html'
524
    model = AgendaPricing
525
    form_class = AgendaPricingForm
526

  
527
    def get_success_url(self):
528
        return reverse('chrono-manager-agenda-pricing-detail', args=[self.agenda.pk, self.object.pk])
529

  
530

  
531
agenda_pricing_add = AgendaPricingAddView.as_view()
532

  
533

  
534
class AgendaPricingDetailView(ViewableAgendaMixin, DetailView):
535
    model = AgendaPricing
536
    pk_url_kwarg = 'pricing_pk'
537
    template_name = 'chrono/pricing/manager_agenda_pricing_detail.html'
538

  
539
    def set_agenda(self, **kwargs):
540
        self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
541

  
542
    def get_queryset(self):
543
        return AgendaPricing.objects.filter(agenda=self.agenda).prefetch_related(
544
            'pricing__criterias__category'
545
        )
546

  
547
    def get_context_data(self, **kwargs):
548
        kwargs['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
549
        return super().get_context_data(**kwargs)
550

  
551

  
552
agenda_pricing_detail = AgendaPricingDetailView.as_view()
553

  
554

  
555
class AgendaPricingEditView(ManagedAgendaMixin, UpdateView):
556
    template_name = 'chrono/pricing/manager_agenda_pricing_form.html'
557
    model = AgendaPricing
558
    pk_url_kwarg = 'pricing_pk'
559
    form_class = AgendaPricingForm
560

  
561
    def set_agenda(self, **kwargs):
562
        self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
563

  
564
    def get_queryset(self):
565
        return AgendaPricing.objects.filter(agenda=self.agenda)
566

  
567
    def get_success_url(self):
568
        return reverse('chrono-manager-agenda-pricing-detail', args=[self.agenda.pk, self.object.pk])
569

  
570

  
571
agenda_pricing_edit = AgendaPricingEditView.as_view()
572

  
573

  
574
class AgendaPricingDeleteView(ManagedAgendaMixin, DeleteView):
575
    template_name = 'chrono/manager_confirm_delete.html'
576
    model = AgendaPricing
577
    pk_url_kwarg = 'pricing_pk'
578

  
579
    def set_agenda(self, **kwargs):
580
        self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
581

  
582
    def get_queryset(self):
583
        return AgendaPricing.objects.filter(agenda=self.agenda)
584

  
585

  
586
agenda_pricing_delete = AgendaPricingDeleteView.as_view()
587

  
588

  
589
class AgendaPricingMatrixEdit(ManagedAgendaMixin, FormView):
590
    template_name = 'chrono/pricing/manager_agenda_pricing_matrix_form.html'
591

  
592
    def set_agenda(self, **kwargs):
593
        self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
594
        self.object = get_object_or_404(
595
            AgendaPricing.objects.filter(agenda=self.agenda), pk=kwargs['pricing_pk']
596
        )
597
        matrix_list = list(self.object.iter_pricing_matrix())
598
        if not matrix_list:
599
            raise Http404
600
        self.matrix = None
601
        if kwargs.get('slug'):
602
            for matrix in matrix_list:
603
                if matrix.criteria is None:
604
                    continue
605
                if matrix.criteria.slug == kwargs['slug']:
606
                    self.matrix = matrix
607
                    break
608
        else:
609
            if matrix_list[0].criteria is None:
610
                self.matrix = matrix_list[0]
611
        if self.matrix is None:
612
            raise Http404
613

  
614
    def get_context_data(self, **kwargs):
615
        kwargs['object'] = self.object
616
        kwargs['matrix'] = self.matrix
617
        return super().get_context_data(**kwargs)
618

  
619
    def get_form(self):
620
        count = len(self.matrix.rows)
621
        PricingMatrixFormSet = forms.formset_factory(
622
            PricingMatrixForm, min_num=count, max_num=count, extra=0, can_delete=False
623
        )
624
        kwargs = {
625
            'initial': [
626
                {'crit_%i' % i: cell.value for i, cell in enumerate(row.cells)} for row in self.matrix.rows
627
            ]
628
        }
629
        if self.request.method == 'POST':
630
            kwargs.update(
631
                {
632
                    'data': self.request.POST,
633
                }
634
            )
635
        return PricingMatrixFormSet(form_kwargs={'matrix': self.matrix}, **kwargs)
636

  
637
    def post(self, *args, **kwargs):
638
        form = self.get_form()
639
        if form.is_valid():
640
            # build prixing_data for this matrix
641
            matrix_pricing_data = defaultdict(dict)
642
            for i, sub_data in enumerate(form.cleaned_data):
643
                row = self.matrix.rows[i]
644
                for j, cell in enumerate(row.cells):
645
                    value = sub_data['crit_%s' % j]
646
                    key = cell.criteria.identifier if cell.criteria else None
647
                    matrix_pricing_data[key][row.criteria.identifier] = float(value)
648
            if self.matrix.criteria:
649
                # full pricing model with 3 categories
650
                self.object.pricing_data = self.object.pricing_data or {}
651
                self.object.pricing_data[self.matrix.criteria.identifier] = matrix_pricing_data
652
            elif list(matrix_pricing_data.keys()) == [None]:
653
                # only one category
654
                self.object.pricing_data = matrix_pricing_data[None]
655
            else:
656
                # 2 categories
657
                self.object.pricing_data = matrix_pricing_data
658
            self.object.save()
659
            return self.form_valid(form)
660
        else:
661
            return self.form_invalid(form)
662

  
663
    def get_success_url(self):
664
        return reverse('chrono-manager-agenda-pricing-detail', args=[self.agenda.pk, self.object.pk])
665

  
666

  
667
agenda_pricing_matrix_edit = AgendaPricingMatrixEdit.as_view()
chrono/settings.py
190 190
    WORKING_DAY_CALENDAR = None
191 191
    EXCEPTIONS_SOURCES = {}
192 192

  
193
# feature flag for publik-famille & pricing developements
194
CHRONO_ENABLE_PRICING = False
195

  
196 193
TEMPLATE_VARS = {}
197 194
SMS_URL = ''
198 195
SMS_SENDER = ''
chrono/urls.py
22 22

  
23 23
from .api.urls import urlpatterns as chrono_api_urls
24 24
from .manager.urls import urlpatterns as chrono_manager_urls
25
from .pricing.urls import urlpatterns as chrono_pricing_urls
26 25
from .urls_utils import decorated_includes
27 26
from .views import LoginView, LogoutView, homepage
28 27

  
29 28
urlpatterns = [
30 29
    url(r'^$', homepage, name='home'),
31 30
    url(r'^manage/', decorated_includes(login_required, include(chrono_manager_urls))),
32
    url(r'^manage/pricing/', decorated_includes(login_required, include(chrono_pricing_urls))),
33 31
    url(r'^api/', include(chrono_api_urls)),
34 32
    url(r'^logout/$', LogoutView.as_view(), name='auth_logout'),
35 33
    url(r'^login/$', LoginView.as_view(), name='auth_login'),
tests/manager/test_all.py
25 25
    UnavailabilityCalendar,
26 26
    VirtualMember,
27 27
)
28
from chrono.pricing.models import AgendaPricing, Pricing
29 28
from chrono.utils.signature import check_query
30 29
from tests.utils import login
31 30

  
......
445 444
    assert 'events_type' not in resp.context['form'].fields
446 445

  
447 446

  
448
def test_options_events_agenda_pricing(settings, app, admin_user):
449
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
450
    pricing1 = Pricing.objects.create(label='Model 1')
451
    pricing2 = Pricing.objects.create(label='Model 2')
452

  
453
    app = login(app)
454
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
455
    assert 'Pricing' in resp
456
    assert 'Model 1' not in resp
457
    assert 'Model 2' not in resp
458
    AgendaPricing.objects.create(
459
        agenda=agenda,
460
        pricing=pricing1,
461
        date_start=datetime.date(year=2021, month=9, day=1),
462
        date_end=datetime.date(year=2022, month=9, day=1),
463
    )
464
    AgendaPricing.objects.create(
465
        agenda=agenda,
466
        pricing=pricing1,
467
        date_start=datetime.date(year=2022, month=9, day=1),
468
        date_end=datetime.date(year=2023, month=9, day=1),
469
    )
470
    AgendaPricing.objects.create(
471
        agenda=agenda,
472
        pricing=pricing2,
473
        date_start=datetime.date(year=2022, month=9, day=1),
474
        date_end=datetime.date(year=2023, month=9, day=1),
475
    )
476

  
477
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
478
    assert 'Model 1 (01/09/2021 - 01/09/2022)' in resp
479
    assert 'Model 1 (01/09/2022 - 01/09/2023)' in resp
480
    assert 'Model 2 (01/09/2022 - 01/09/2023)' in resp
481

  
482
    settings.CHRONO_ENABLE_PRICING = False
483
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
484
    assert 'Pricing' not in resp
485
    assert 'Model 1' not in resp
486
    assert 'Model 2' not in resp
487
    settings.CHRONO_ENABLE_PRICING = True
488

  
489
    # check kind
490
    agenda.kind = 'meetings'
491
    agenda.save()
492
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
493
    assert 'Pricing' not in resp
494
    assert 'Model 1' not in resp
495
    assert 'Model 2' not in resp
496

  
497

  
498 447
def test_options_events_agenda_delays(settings, app, admin_user):
499 448
    settings.WORKING_DAY_CALENDAR = None
500 449
    agenda = Agenda.objects.create(label='Foo bar')
tests/manager/test_check_type.py
119 119
    assert check_type.group == group
120 120
    assert check_type.slug == 'foo-reason'
121 121
    assert check_type.kind == 'absence'
122
    assert check_type.pricing is None
123
    assert check_type.pricing_rate is None
124 122

  
125 123
    resp = app.get('/manage/check-type/group/%s/add/' % group.pk)
126 124
    resp.form['label'] = 'Foo reason'
......
133 131
    assert check_type.kind == 'presence'
134 132

  
135 133

  
136
def test_add_check_type_pricing(settings, app, admin_user):
137
    group = CheckTypeGroup.objects.create(label='Foo bar')
138

  
139
    app = login(app)
140
    resp = app.get('/manage/check-type/group/%s/add/' % group.pk)
141
    assert 'pricing' in resp.context['form'].fields
142
    assert 'pricing_rate' in resp.context['form'].fields
143
    resp.form['label'] = 'Foo reason'
144
    resp.form['pricing'] = 42
145
    resp.form['pricing_rate'] = 150
146
    resp = resp.form.submit()
147
    assert resp.context['form'].errors['__all__'] == ['Please choose between pricing and pricing rate.']
148
    resp.form['pricing'] = 0
149
    resp.form['pricing_rate'] = 0
150
    resp = resp.form.submit()
151
    assert resp.context['form'].errors['__all__'] == ['Please choose between pricing and pricing rate.']
152
    resp.form['pricing'] = 42
153
    resp.form['pricing_rate'] = None
154
    resp = resp.form.submit()
155
    check_type = CheckType.objects.latest('pk')
156
    assert check_type.pricing == 42
157
    assert check_type.pricing_rate is None
158

  
159
    resp = app.get('/manage/check-type/group/%s/add/' % group.pk)
160
    resp.form['label'] = 'Foo reason'
161
    resp.form['pricing_rate'] = 150
162
    resp = resp.form.submit()
163
    check_type = CheckType.objects.latest('pk')
164
    assert check_type.pricing is None
165
    assert check_type.pricing_rate == 150
166

  
167
    settings.CHRONO_ENABLE_PRICING = False
168
    resp = app.get('/manage/check-type/group/%s/add/' % group.pk)
169
    assert 'pricing' not in resp.context['form'].fields
170
    assert 'pricing_rate' not in resp.context['form'].fields
171

  
172

  
173 134
def test_add_check_type_as_manager(app, manager_user, agenda_with_restrictions):
174 135
    group = CheckTypeGroup.objects.create(label='Foo bar')
175 136

  
......
205 166
    assert check_type.label == 'Foo bar reason'
206 167
    assert check_type.slug == 'foo-bar-reason'
207 168
    assert check_type.kind == 'presence'
208
    assert check_type.pricing is None
209
    assert check_type.pricing_rate is None
210 169
    assert check_type.disabled is True
211 170

  
212 171
    # check_type is used
......
225 184
    app.get('/manage/check-type/group/%s/%s/edit/' % (group.pk, check_type.pk), status=403)
226 185

  
227 186

  
228
def test_edit_check_type_pricing(settings, app, admin_user):
229
    group = CheckTypeGroup.objects.create(label='Foo bar')
230
    check_type = CheckType.objects.create(label='Foo reason', group=group)
231

  
232
    app = login(app)
233
    resp = app.get('/manage/check-type/group/%s/%s/edit/' % (group.pk, check_type.pk))
234
    assert 'pricing' in resp.context['form'].fields
235
    assert 'pricing_rate' in resp.context['form'].fields
236
    resp.form['pricing'] = 42
237
    resp.form['pricing_rate'] = 150
238
    resp = resp.form.submit()
239
    assert resp.context['form'].errors['__all__'] == ['Please choose between pricing and pricing rate.']
240
    resp.form['pricing'] = 42
241
    resp.form['pricing_rate'] = None
242
    resp = resp.form.submit()
243
    check_type.refresh_from_db()
244
    assert check_type.pricing == 42
245
    assert check_type.pricing_rate is None
246

  
247
    resp = app.get('/manage/check-type/group/%s/%s/edit/' % (group.pk, check_type.pk))
248
    resp.form['pricing'] = None
249
    resp.form['pricing_rate'] = 150
250
    resp = resp.form.submit()
251
    check_type.refresh_from_db()
252
    assert check_type.pricing is None
253
    assert check_type.pricing_rate == 150
254

  
255
    settings.CHRONO_ENABLE_PRICING = False
256
    resp = app.get('/manage/check-type/group/%s/%s/edit/' % (group.pk, check_type.pk))
257
    assert 'pricing' not in resp.context['form'].fields
258
    assert 'pricing_rate' not in resp.context['form'].fields
259

  
260

  
261 187
def test_delete_check_type(app, admin_user):
262 188
    group = CheckTypeGroup.objects.create(label='Foo bar')
263 189
    check_type = CheckType.objects.create(label='Foo reason', group=group)
tests/manager/test_import_export.py
22 22
pytestmark = pytest.mark.django_db
23 23

  
24 24

  
25
def test_export_site(settings, app, admin_user):
25
def test_export_site(app, admin_user):
26 26
    login(app)
27 27
    resp = app.get('/manage/')
28 28
    resp = resp.click('Export')
......
40 40
        'events_types': [],
41 41
        'resources': [],
42 42
        'categories': [],
43
        'pricing_categories': [],
44
        'pricing_models': [],
45 43
    }
46 44

  
47 45
    agenda = Agenda.objects.create(label='Foo Bar', kind='events')
......
57 55
    assert len(site_json['events_types']) == 0
58 56
    assert len(site_json['resources']) == 0
59 57
    assert len(site_json['categories']) == 0
60
    assert len(site_json['pricing_categories']) == 0
61
    assert len(site_json['pricing_models']) == 0
62 58

  
63 59
    resp = app.get('/manage/agendas/export/')
64 60
    resp.form['agendas'] = False
......
66 62
    resp.form['events_types'] = False
67 63
    resp.form['resources'] = False
68 64
    resp.form['categories'] = False
69
    resp.form['pricing_categories'] = False
70
    resp.form['pricing_models'] = False
71 65
    resp = resp.form.submit()
72 66

  
73 67
    site_json = json.loads(resp.text)
......
77 71
    assert 'events_types' not in site_json
78 72
    assert 'resources' not in site_json
79 73
    assert 'categories' not in site_json
80
    assert 'pricing_categories' not in site_json
81
    assert 'pricing_models' not in site_json
82

  
83
    settings.CHRONO_ENABLE_PRICING = False
84
    resp = app.get('/manage/agendas/export/')
85
    assert 'pricing_categories' not in resp.context['form'].fields
86
    assert 'pricing_models' not in resp.context['form'].fields
87 74

  
88 75

  
89 76
def test_import_agenda_as_manager(app, manager_user):
tests/pricing/conftest.py
1
import pytest
2
from django.contrib.auth.models import Group, User
3

  
4

  
5
@pytest.fixture
6
def simple_user():
7
    try:
8
        user = User.objects.get(username='user')
9
    except User.DoesNotExist:
10
        user = User.objects.create_user('user', password='user')
11
    return user
12

  
13

  
14
@pytest.fixture
15
def managers_group():
16
    group, _ = Group.objects.get_or_create(name='Managers')
17
    return group
18

  
19

  
20
@pytest.fixture
21
def manager_user(managers_group):
22
    try:
23
        user = User.objects.get(username='manager')
24
    except User.DoesNotExist:
25
        user = User.objects.create_user('manager', password='manager')
26
    user.groups.set([managers_group])
27
    return user
28

  
29

  
30
@pytest.fixture
31
def admin_user():
32
    try:
33
        user = User.objects.get(username='admin')
34
    except User.DoesNotExist:
35
        user = User.objects.create_superuser('admin', email=None, password='admin')
36
    return user
tests/pricing/test_manager.py
1
import copy
2
import datetime
3
import json
4

  
5
import pytest
6
from webtest import Upload
7

  
8
from chrono.agendas.models import Agenda
9
from chrono.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
10
from tests.utils import login
11

  
12
pytestmark = pytest.mark.django_db
13

  
14

  
15
@pytest.fixture
16
def agenda_with_restrictions(manager_user):
17
    agenda = Agenda(label='Foo Bar')
18
    agenda.view_role = manager_user.groups.all()[0]
19
    agenda.save()
20
    return agenda
21

  
22

  
23
def test_list_pricings_as_manager(app, manager_user, agenda_with_restrictions):
24
    app = login(app, username='manager', password='manager')
25
    app.get('/manage/pricing/', status=403)
26

  
27
    resp = app.get('/manage/')
28
    assert 'Pricing' not in resp.text
29

  
30

  
31
def test_add_pricing(settings, app, admin_user):
32
    app = login(app)
33
    resp = app.get('/manage/')
34
    resp = resp.click('Pricing')
35
    resp = resp.click('New pricing model')
36
    resp.form['label'] = 'Pricing model for lunch'
37
    resp = resp.form.submit()
38
    pricing = Pricing.objects.latest('pk')
39
    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
40
    assert pricing.label == 'Pricing model for lunch'
41
    assert pricing.slug == 'pricing-model-for-lunch'
42

  
43
    settings.CHRONO_ENABLE_PRICING = False
44
    resp = app.get('/manage/')
45
    assert 'Pricing' not in resp.text
46

  
47

  
48
def test_add_pricing_as_manager(app, manager_user):
49
    app = login(app, username='manager', password='manager')
50
    app.get('/manage/pricing/add/', status=403)
51

  
52

  
53
def test_detail_pricing(app, admin_user):
54
    pricing = Pricing.objects.create(label='Model')
55

  
56
    app = login(app)
57
    resp = app.get('/manage/pricing/')
58
    resp = resp.click(href='/manage/pricing/%s/' % pricing.pk)
59
    assert '/manage/pricing/%s/edit/' % pricing.pk in resp
60
    assert '/manage/pricing/%s/delete/' % pricing.pk in resp
61

  
62

  
63
def test_detail_pricing_as_manager(app, manager_user):
64
    pricing = Pricing.objects.create(label='Model')
65

  
66
    app = login(app, username='manager', password='manager')
67
    app.get('/manage/pricing/%s/' % pricing.pk, status=403)
68

  
69

  
70
def test_edit_pricing(app, admin_user):
71
    pricing = Pricing.objects.create(label='Model 1')
72
    pricing2 = Pricing.objects.create(label='Model 2')
73

  
74
    app = login(app)
75
    resp = app.get('/manage/pricing/%s/' % pricing.pk)
76
    resp = resp.click(href='/manage/pricing/%s/edit/' % pricing.pk)
77
    resp.form['label'] = 'Model Foo'
78
    resp.form['slug'] = pricing2.slug
79
    resp = resp.form.submit()
80
    assert resp.context['form'].errors['slug'] == ['Pricing with this Identifier already exists.']
81

  
82
    resp.form['slug'] = 'foo-bar'
83
    resp = resp.form.submit()
84
    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
85
    pricing.refresh_from_db()
86
    assert pricing.label == 'Model Foo'
87
    assert pricing.slug == 'foo-bar'
88

  
89

  
90
def test_edit_pricing_as_manager(app, manager_user):
91
    pricing = Pricing.objects.create(label='Model')
92

  
93
    app = login(app, username='manager', password='manager')
94
    app.get('/manage/pricing/%s/edit/' % pricing.pk, status=403)
95

  
96

  
97
def test_delete_pricing(app, admin_user):
98
    pricing = Pricing.objects.create(label='Model')
99

  
100
    app = login(app)
101
    resp = app.get('/manage/pricing/%s/' % pricing.pk)
102
    resp = resp.click(href='/manage/pricing/%s/delete/' % pricing.pk)
103
    resp = resp.form.submit()
104
    assert resp.location.endswith('/manage/pricing/')
105
    assert Pricing.objects.exists() is False
106

  
107

  
108
def test_delete_pricing_as_manager(app, manager_user):
109
    pricing = Pricing.objects.create(label='Model')
110

  
111
    app = login(app, username='manager', password='manager')
112
    app.get('/manage/pricing/%s/delete/' % pricing.pk, status=403)
113

  
114

  
115
def test_duplicate_pricing(app, admin_user):
116
    pricing = Pricing.objects.create(label='Model')
117
    assert Pricing.objects.count() == 1
118

  
119
    app = login(app)
120
    resp = app.get('/manage/pricing/%s/' % pricing.pk)
121
    resp = resp.click(href='/manage/pricing/%s/duplicate/' % pricing.pk)
122
    resp = resp.form.submit()
123
    assert Pricing.objects.count() == 2
124

  
125
    new_pricing = Pricing.objects.latest('pk')
126
    assert resp.location == '/manage/pricing/%s/' % new_pricing.pk
127
    assert new_pricing.pk != pricing.pk
128

  
129
    resp = resp.follow()
130
    assert 'copy-of-model' in resp.text
131

  
132
    resp = resp.click(href='/manage/pricing/%s/duplicate/' % new_pricing.pk)
133
    resp.form['label'] = 'hop'
134
    resp = resp.form.submit().follow()
135
    assert 'hop' in resp.text
136

  
137

  
138
def test_duplicate_pricing_as_manager(app, manager_user):
139
    pricing = Pricing.objects.create(label='Model')
140

  
141
    app = login(app, username='manager', password='manager')
142
    app.get('/manage/pricing/%s/duplicate/' % pricing.pk, status=403)
143

  
144

  
145
@pytest.mark.freeze_time('2021-07-08')
146
def test_import_pricing(app, admin_user):
147
    pricing = Pricing.objects.create(label='Model')
148

  
149
    app = login(app)
150
    resp = app.get('/manage/pricing/%s/export/' % pricing.id)
151
    assert resp.headers['content-type'] == 'application/json'
152
    assert resp.headers['content-disposition'] == 'attachment; filename="export_pricing_model_20210708.json"'
153
    pricing_export = resp.text
154

  
155
    # existing pricing
156
    resp = app.get('/manage/', status=200)
157
    resp = resp.click('Import')
158
    resp.form['agendas_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
159
    resp = resp.form.submit()
160
    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
161
    resp = resp.follow()
162
    assert 'No pricing model created. A pricing model has been updated.' not in resp.text
163
    assert Pricing.objects.count() == 1
164

  
165
    # new pricing
166
    Pricing.objects.all().delete()
167
    resp = app.get('/manage/', status=200)
168
    resp = resp.click('Import')
169
    resp.form['agendas_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
170
    resp = resp.form.submit()
171
    pricing = Pricing.objects.latest('pk')
172
    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
173
    resp = resp.follow()
174
    assert 'A pricing model has been created. No pricing model updated.' not in resp.text
175
    assert Pricing.objects.count() == 1
176

  
177
    # multiple pricing
178
    pricings = json.loads(pricing_export)
179
    pricings['pricing_models'].append(copy.copy(pricings['pricing_models'][0]))
180
    pricings['pricing_models'].append(copy.copy(pricings['pricing_models'][0]))
181
    pricings['pricing_models'][1]['label'] = 'Foo bar 2'
182
    pricings['pricing_models'][1]['slug'] = 'foo-bar-2'
183
    pricings['pricing_models'][2]['label'] = 'Foo bar 3'
184
    pricings['pricing_models'][2]['slug'] = 'foo-bar-3'
185

  
186
    resp = app.get('/manage/', status=200)
187
    resp = resp.click('Import')
188
    resp.form['agendas_json'] = Upload(
189
        'export.json', json.dumps(pricings).encode('utf-8'), 'application/json'
190
    )
191
    resp = resp.form.submit()
192
    assert resp.location.endswith('/manage/')
193
    resp = resp.follow()
194
    assert '2 pricing models have been created. A pricing model has been updated.' in resp.text
195
    assert Pricing.objects.count() == 3
196

  
197
    Pricing.objects.all().delete()
198
    resp = app.get('/manage/', status=200)
199
    resp = resp.click('Import')
200
    resp.form['agendas_json'] = Upload(
201
        'export.json', json.dumps(pricings).encode('utf-8'), 'application/json'
202
    )
203
    resp = resp.form.submit().follow()
204
    assert '3 pricing models have been created. No pricing model updated.' in resp.text
205
    assert Pricing.objects.count() == 3
206

  
207

  
208
def test_pricing_edit_extra_variables(app, admin_user):
209
    pricing = Pricing.objects.create(label='Model')
210
    assert pricing.extra_variables == {}
211

  
212
    app = login(app)
213
    resp = app.get('/manage/pricing/%s/' % pricing.id)
214
    assert '<label>Extra variables:</label>' not in resp.text
215
    resp = resp.click(href='/manage/pricing/%s/variable/' % pricing.id)
216
    resp.form['form-0-key'] = 'foo'
217
    resp.form['form-0-value'] = 'bar'
218
    resp = resp.form.submit().follow()
219
    pricing.refresh_from_db()
220
    assert pricing.extra_variables == {'foo': 'bar'}
221
    assert '<label>Extra variables:</label>' in resp.text
222
    assert '<i>foo</i>' in resp
223

  
224
    resp = resp.click(href='/manage/pricing/%s/variable/' % pricing.id)
225
    assert resp.form['form-TOTAL_FORMS'].value == '2'
226
    assert resp.form['form-0-key'].value == 'foo'
227
    assert resp.form['form-0-value'].value == 'bar'
228
    assert resp.form['form-1-key'].value == ''
229
    assert resp.form['form-1-value'].value == ''
230
    resp.form['form-0-value'] = 'bar-bis'
231
    resp.form['form-1-key'] = 'blah'
232
    resp.form['form-1-value'] = 'baz'
233
    resp = resp.form.submit().follow()
234
    pricing.refresh_from_db()
235
    assert pricing.extra_variables == {
236
        'foo': 'bar-bis',
237
        'blah': 'baz',
238
    }
239
    assert '<i>blah</i>, <i>foo</i>' in resp
240

  
241
    resp = resp.click(href='/manage/pricing/%s/variable/' % pricing.id)
242
    assert resp.form['form-TOTAL_FORMS'].value == '3'
243
    assert resp.form['form-0-key'].value == 'blah'
244
    assert resp.form['form-0-value'].value == 'baz'
245
    assert resp.form['form-1-key'].value == 'foo'
246
    assert resp.form['form-1-value'].value == 'bar-bis'
247
    assert resp.form['form-2-key'].value == ''
248
    assert resp.form['form-2-value'].value == ''
249
    resp.form['form-1-key'] = 'foo'
250
    resp.form['form-1-value'] = 'bar'
251
    resp.form['form-0-key'] = ''
252
    resp = resp.form.submit().follow()
253
    pricing.refresh_from_db()
254
    assert pricing.extra_variables == {
255
        'foo': 'bar',
256
    }
257
    assert '<i>foo</i>' in resp
258

  
259

  
260
def test_pricing_edit_extra_variables_as_manager(app, manager_user):
261
    pricing = Pricing.objects.create(label='Model')
262

  
263
    app = login(app, username='manager', password='manager')
264
    app.get('/manage/pricing/%s/variable/' % pricing.pk, status=403)
265

  
266

  
267
def test_pricing_add_category(app, admin_user):
268
    pricing = Pricing.objects.create(label='Model')
269
    category1 = CriteriaCategory.objects.create(label='Cat 1')
270
    category2 = CriteriaCategory.objects.create(label='Cat 2')
271
    category3 = CriteriaCategory.objects.create(label='Cat 3')
272
    category4 = CriteriaCategory.objects.create(label='Cat 4')
273

  
274
    app = login(app)
275
    resp = app.get('/manage/pricing/%s/' % pricing.pk)
276
    resp = resp.click(href='/manage/pricing/%s/category/add/' % pricing.pk)
277
    assert list(resp.context['form'].fields['category'].queryset) == [
278
        category1,
279
        category2,
280
        category3,
281
        category4,
282
    ]
283
    resp.form['category'] = category1.pk
284
    resp = resp.form.submit()
285
    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
286
    resp = resp.follow()
287
    assert '/manage/pricing/%s/category/add/' % pricing.pk in resp
288
    assert list(
289
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
290
    ) == [category1.pk]
291
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
292
        1
293
    ]
294

  
295
    resp = app.get('/manage/pricing/%s/category/add/' % pricing.pk)
296
    assert list(resp.context['form'].fields['category'].queryset) == [category2, category3, category4]
297
    resp.form['category'] = category4.pk
298
    resp = resp.form.submit().follow()
299
    assert list(
300
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
301
    ) == [category1.pk, category4.pk]
302
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
303
        1,
304
        2,
305
    ]
306

  
307
    resp = app.get('/manage/pricing/%s/category/add/' % pricing.pk)
308
    assert list(resp.context['form'].fields['category'].queryset) == [category2, category3]
309
    resp.form['category'] = category2.pk
310
    resp = resp.form.submit().follow()
311
    assert '/manage/pricing/%s/category/add/' % pricing.pk not in resp
312
    assert list(
313
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
314
    ) == [category1.pk, category4.pk, category2.pk]
315
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
316
        1,
317
        2,
318
        3,
319
    ]
320

  
321
    app.get('/manage/pricing/%s/category/add/' % pricing.pk, status=404)
322

  
323

  
324
def test_pricing_add_category_as_manager(app, manager_user):
325
    pricing = Pricing.objects.create(label='Model')
326

  
327
    app = login(app, username='manager', password='manager')
328
    app.get('/manage/pricing/%s/category/add/' % pricing.pk, status=403)
329

  
330

  
331
def test_pricing_edit_category(app, admin_user):
332
    category1 = CriteriaCategory.objects.create(label='Cat 1')
333
    criteria1 = Criteria.objects.create(label='Crit 1', category=category1)
334
    criteria2 = Criteria.objects.create(label='Crit 2', category=category1)
335
    criteria3 = Criteria.objects.create(label='Crit 3', category=category1)
336
    criteria4 = Criteria.objects.create(label='Crit 4', category=category1)
337
    category2 = CriteriaCategory.objects.create(label='cat 2')
338
    criteria5 = Criteria.objects.create(label='Crit 5', category=category2)
339
    pricing = Pricing.objects.create(label='Model')
340
    pricing.categories.add(category1, through_defaults={'order': 1})
341
    pricing.categories.add(category2, through_defaults={'order': 2})
342

  
343
    app = login(app)
344
    resp = app.get('/manage/pricing/%s/' % pricing.pk)
345
    resp = resp.click(href='/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category1.pk))
346
    assert list(resp.context['form'].fields['criterias'].queryset) == [
347
        criteria1,
348
        criteria2,
349
        criteria3,
350
        criteria4,
351
    ]
352
    assert list(resp.context['form'].initial['criterias']) == []
353
    resp.form['criterias'] = [criteria1.pk, criteria3.pk]
354
    resp = resp.form.submit()
355
    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
356
    resp = resp.follow()
357
    assert list(pricing.criterias.order_by('pk')) == [criteria1, criteria3]
358

  
359
    resp = app.get('/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category1.pk))
360
    assert list(resp.context['form'].initial['criterias']) == [criteria1, criteria3]
361
    resp.form['criterias'] = [criteria1.pk, criteria4.pk]
362
    resp = resp.form.submit().follow()
363
    assert list(pricing.criterias.order_by('pk')) == [criteria1, criteria4]
364

  
365
    resp = app.get('/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category2.pk))
366
    assert list(resp.context['form'].fields['criterias'].queryset) == [criteria5]
367
    assert list(resp.context['form'].initial['criterias']) == []
368
    resp.form['criterias'] = [criteria5.pk]
369
    resp = resp.form.submit().follow()
370
    assert list(pricing.criterias.order_by('pk')) == [criteria1, criteria4, criteria5]
371

  
372

  
373
def test_pricing_edit_category_as_manager(app, manager_user):
374
    pricing = Pricing.objects.create(label='Model')
375
    category = CriteriaCategory.objects.create(label='Cat')
376
    pricing.categories.add(category, through_defaults={'order': 1})
377

  
378
    app = login(app, username='manager', password='manager')
379
    app.get('/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category.pk), status=403)
380

  
381

  
382
def test_pricing_delete_category(app, admin_user):
383
    category1 = CriteriaCategory.objects.create(label='Cat 1')
384
    criteria1 = Criteria.objects.create(label='Crit 1', category=category1)
385
    category2 = CriteriaCategory.objects.create(label='Cat 2')
386
    criteria2 = Criteria.objects.create(label='Crit 2', category=category2)
387
    pricing = Pricing.objects.create(label='Model')
388
    pricing.categories.add(category1, through_defaults={'order': 1})
389
    pricing.categories.add(category2, through_defaults={'order': 2})
390
    pricing.criterias.add(criteria1, criteria2)
391

  
392
    app = login(app)
393
    resp = app.get('/manage/pricing/%s/' % pricing.pk)
394
    resp = resp.click(href='/manage/pricing/%s/category/%s/delete/' % (pricing.pk, category1.pk))
395
    resp = resp.form.submit()
396
    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
397
    resp = resp.follow()
398
    assert list(pricing.categories.all()) == [category2]
399
    assert list(pricing.criterias.all()) == [criteria2]
400

  
401
    # not linked
402
    app.get('/manage/pricing/%s/category/%s/delete/' % (pricing.pk, category1.pk), status=404)
403
    # unknown
404
    app.get('/manage/pricing/%s/category/%s/delete/' % (pricing.pk, 0), status=404)
405

  
406

  
407
def test_pricing_delete_category_as_manager(app, manager_user):
408
    pricing = Pricing.objects.create(label='Model')
409
    category = CriteriaCategory.objects.create(label='Cat')
410

  
411
    app = login(app, username='manager', password='manager')
412
    app.get('/manage/pricing/%s/category/%s/delete/' % (pricing.pk, category.pk), status=403)
413

  
414

  
415
def test_pricing_reorder_categories(app, admin_user):
416
    category1 = CriteriaCategory.objects.create(label='Cat 1')
417
    category2 = CriteriaCategory.objects.create(label='Cat 2')
418
    category3 = CriteriaCategory.objects.create(label='Cat 3')
419
    category4 = CriteriaCategory.objects.create(label='Cat 4')
420
    pricing = Pricing.objects.create(label='Model')
421
    pricing.categories.add(category1, through_defaults={'order': 1})
422
    pricing.categories.add(category2, through_defaults={'order': 2})
423
    pricing.categories.add(category3, through_defaults={'order': 3})
424
    assert list(
425
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
426
    ) == [category1.pk, category2.pk, category3.pk]
427
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
428
        1,
429
        2,
430
        3,
431
    ]
432

  
433
    app = login(app)
434
    # missing get params
435
    app.get('/manage/pricing/%s/order/' % (pricing.pk), status=400)
436

  
437
    # bad new-order param
438
    bad_params = [
439
        # missing category3 in order
440
        ','.join(str(x) for x in [category1.pk, category2.pk]),
441
        # category1 mentionned twice
442
        ','.join(str(x) for x in [category1.pk, category2.pk, category3.pk, category1.pk]),
443
        # category4 not in pricing categories
444
        ','.join(str(x) for x in [category1.pk, category2.pk, category3.pk, category4.pk]),
445
        # not an id
446
        'foo,1,2,3',
447
        ' 1 ,2,3',
448
    ]
449
    for bad_param in bad_params:
450
        app.get(
451
            '/manage/pricing/%s/order/' % (pricing.pk),
452
            params={'new-order': bad_param},
453
            status=400,
454
        )
455
    # not changed
456
    assert list(
457
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
458
    ) == [category1.pk, category2.pk, category3.pk]
459
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
460
        1,
461
        2,
462
        3,
463
    ]
464

  
465
    # change order
466
    app.get(
467
        '/manage/pricing/%s/order/' % (pricing.pk),
468
        params={'new-order': ','.join(str(x) for x in [category3.pk, category1.pk, category2.pk])},
469
    )
470
    assert list(
471
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
472
    ) == [category3.pk, category1.pk, category2.pk]
473
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
474
        1,
475
        2,
476
        3,
477
    ]
478

  
479

  
480
def test_pricing_reorder_categories_as_manager(app, manager_user):
481
    pricing = Pricing.objects.create(label='Model')
482

  
483
    app = login(app, username='manager', password='manager')
484
    app.get('/manage/pricing/%s/order/' % (pricing.pk), status=403)
485

  
486

  
487
def test_list_criterias_as_manager(app, manager_user):
488
    app = login(app, username='manager', password='manager')
489
    app.get('/manage/pricing/criterias/', status=403)
490

  
491

  
492
def test_add_category(settings, app, admin_user):
493
    app = login(app)
494
    resp = app.get('/manage/')
495
    resp = resp.click('Pricing')
496
    resp = resp.click('Criterias')
497
    resp = resp.click('New category')
498
    resp.form['label'] = 'QF'
499
    resp = resp.form.submit()
500
    category = CriteriaCategory.objects.latest('pk')
501
    assert resp.location.endswith('/manage/pricing/criterias/')
502
    assert category.label == 'QF'
503
    assert category.slug == 'qf'
504

  
505

  
506
def test_add_category_as_manager(app, manager_user):
507
    app = login(app, username='manager', password='manager')
508
    app.get('/manage/pricing/criteria/category/add/', status=403)
509

  
510

  
511
def test_edit_category(app, admin_user):
512
    category = CriteriaCategory.objects.create(label='QF')
513
    category2 = CriteriaCategory.objects.create(label='Domicile')
514

  
515
    app = login(app)
516
    resp = app.get('/manage/pricing/criterias/')
517
    resp = resp.click(href='/manage/pricing/criteria/category/%s/edit/' % category.pk)
518
    resp.form['label'] = 'QF Foo'
519
    resp.form['slug'] = category2.slug
520
    resp = resp.form.submit()
521
    assert resp.context['form'].errors['slug'] == ['Criteria category with this Identifier already exists.']
522

  
523
    resp.form['slug'] = 'baz2'
524
    resp = resp.form.submit()
525
    assert resp.location.endswith('/manage/pricing/criterias/')
526
    category.refresh_from_db()
527
    assert category.label == 'QF Foo'
528
    assert category.slug == 'baz2'
529

  
530

  
531
def test_edit_category_as_manager(app, manager_user):
532
    category = CriteriaCategory.objects.create(label='Foo bar')
533

  
534
    app = login(app, username='manager', password='manager')
535
    app.get('/manage/pricing/criteria/category/%s/edit/' % category.pk, status=403)
536

  
537

  
538
def test_delete_category(app, admin_user):
539
    category = CriteriaCategory.objects.create(label='QF')
540
    Criteria.objects.create(label='QF 1', category=category)
541

  
542
    app = login(app)
543
    resp = app.get('/manage/pricing/criterias/')
544
    resp = resp.click(href='/manage/pricing/criteria/category/%s/delete/' % category.pk)
545
    resp = resp.form.submit()
546
    assert resp.location.endswith('/manage/pricing/criterias/')
547
    assert CriteriaCategory.objects.exists() is False
548
    assert Criteria.objects.exists() is False
549

  
550

  
551
def test_delete_category_as_manager(app, manager_user):
552
    category = CriteriaCategory.objects.create(label='Foo bar')
553

  
554
    app = login(app, username='manager', password='manager')
555
    app.get('/manage/pricing/criteria/category/%s/delete/' % category.pk, status=403)
556

  
557

  
558
def test_add_criteria(app, admin_user):
559
    category = CriteriaCategory.objects.create(label='QF')
560

  
561
    app = login(app)
562
    resp = app.get('/manage/pricing/criterias/')
563
    resp = resp.click('Add a criteria')
564
    resp.form['label'] = 'QF < 1'
565
    resp.form['condition'] = 'qf < 1 #'
566
    assert 'slug' not in resp.context['form'].fields
567
    resp = resp.form.submit()
568
    assert resp.context['form'].errors['condition'] == ['Invalid syntax.']
569
    resp.form['condition'] = 'qf < 1'
570
    resp = resp.form.submit()
571
    criteria = Criteria.objects.latest('pk')
572
    assert resp.location.endswith('/manage/pricing/criterias/')
573
    assert criteria.label == 'QF < 1'
574
    assert criteria.category == category
575
    assert criteria.slug == 'qf-1'
576
    assert criteria.condition == 'qf < 1'
577
    assert criteria.order == 1
578

  
579
    resp = app.get('/manage/pricing/criteria/category/%s/add/' % category.pk)
580
    resp.form['label'] = 'QF < 1'
581
    resp.form['condition'] = 'qf < 1'
582
    resp = resp.form.submit()
583
    assert resp.location.endswith('/manage/pricing/criterias/')
584
    criteria = Criteria.objects.latest('pk')
585
    assert criteria.label == 'QF < 1'
586
    assert criteria.category == category
587
    assert criteria.slug == 'qf-1-1'
588
    assert criteria.condition == 'qf < 1'
589
    assert criteria.order == 2
590

  
591

  
592
def test_add_criteria_as_manager(app, manager_user):
593
    category = CriteriaCategory.objects.create(label='Foo bar')
594

  
595
    app = login(app, username='manager', password='manager')
596
    app.get('/manage/pricing/criteria/category/%s/add/' % category.pk, status=403)
597

  
598

  
599
def test_edit_criteria(app, admin_user):
600
    category = CriteriaCategory.objects.create(label='QF')
601
    criteria = Criteria.objects.create(label='QF 1', category=category)
602
    criteria2 = Criteria.objects.create(label='QF 2', category=category)
603
    category2 = CriteriaCategory.objects.create(label='Foo')
604
    criteria3 = Criteria.objects.create(label='foo-bar', category=category2)
605

  
606
    app = login(app)
607
    resp = app.get('/manage/pricing/criterias/')
608
    resp = resp.click(href='/manage/pricing/criteria/category/%s/%s/edit/' % (category.pk, criteria.pk))
609
    resp.form['label'] = 'QF 1 bis'
610
    resp.form['slug'] = criteria2.slug
611
    resp.form['condition'] = 'qf <= 1 #'
612
    resp = resp.form.submit()
613
    assert resp.context['form'].errors['slug'] == ['Another criteria exists with the same identifier.']
614
    assert resp.context['form'].errors['condition'] == ['Invalid syntax.']
615

  
616
    resp.form['condition'] = 'qf <= 1'
617
    resp.form['slug'] = criteria3.slug
618
    resp = resp.form.submit()
619
    assert resp.location.endswith('/manage/pricing/criterias/')
620
    criteria.refresh_from_db()
621
    assert criteria.label == 'QF 1 bis'
622
    assert criteria.slug == 'foo-bar'
623
    assert criteria.condition == 'qf <= 1'
624

  
625

  
626
def test_edit_criteria_as_manager(app, manager_user):
627
    category = CriteriaCategory.objects.create(label='QF')
628
    criteria = Criteria.objects.create(label='QF 1', category=category)
629

  
630
    app = login(app, username='manager', password='manager')
631
    app.get('/manage/pricing/criteria/category/%s/%s/edit/' % (category.pk, criteria.pk), status=403)
632

  
633

  
634
def test_delete_criteria(app, admin_user):
635
    category = CriteriaCategory.objects.create(label='QF')
636
    criteria = Criteria.objects.create(label='QF 1', category=category)
637

  
638
    app = login(app)
639
    resp = app.get('/manage/pricing/criterias/')
640
    resp = resp.click(href='/manage/pricing/criteria/category/%s/%s/delete/' % (category.pk, criteria.pk))
641
    resp = resp.form.submit()
642
    assert resp.location.endswith('/manage/pricing/criterias/')
643
    assert CriteriaCategory.objects.exists() is True
644
    assert Criteria.objects.exists() is False
645

  
646

  
647
def test_delete_criteria_as_manager(app, manager_user):
648
    category = CriteriaCategory.objects.create(label='QF')
649
    criteria = Criteria.objects.create(label='QF 1', category=category)
650

  
651
    app = login(app, username='manager', password='manager')
652
    app.get('/manage/pricing/criteria/category/%s/%s/delete/' % (category.pk, criteria.pk), status=403)
653

  
654

  
655
def test_reorder_criterias(app, admin_user):
656
    category = CriteriaCategory.objects.create(label='QF')
657
    criteria1 = Criteria.objects.create(label='QF 1', category=category)
658
    criteria2 = Criteria.objects.create(label='QF 2', category=category)
659
    criteria3 = Criteria.objects.create(label='QF 3', category=category)
660
    criteria4 = Criteria.objects.create(label='QF 4', category=category)
661
    assert list(category.criterias.values_list('pk', flat=True).order_by('order')) == [
662
        criteria1.pk,
663
        criteria2.pk,
664
        criteria3.pk,
665
        criteria4.pk,
666
    ]
667
    assert list(category.criterias.values_list('order', flat=True).order_by('order')) == [1, 2, 3, 4]
668

  
669
    app = login(app)
670
    # missing get params
671
    app.get('/manage/pricing/criteria/category/%s/order/' % (category.pk), status=400)
672

  
673
    # bad new-order param
674
    bad_params = [
675
        # missing criteria3 in order
676
        ','.join(str(x) for x in [criteria1.pk, criteria2.pk, criteria4.pk]),
677
        # criteria1 mentionned twice
678
        ','.join(str(x) for x in [criteria1.pk, criteria2.pk, criteria3.pk, criteria4.pk, criteria1.pk]),
679
        # not an id
680
        'foo,1,2,3,4',
681
        ' 1 ,2,3,4',
682
    ]
683
    for bad_param in bad_params:
684
        app.get(
685
            '/manage/pricing/criteria/category/%s/order/' % (category.pk),
686
            params={'new-order': bad_param},
687
            status=400,
688
        )
689
    # not changed
690
    assert list(category.criterias.values_list('pk', flat=True).order_by('order')) == [
691
        criteria1.pk,
692
        criteria2.pk,
693
        criteria3.pk,
694
        criteria4.pk,
695
    ]
696
    assert list(category.criterias.values_list('order', flat=True).order_by('order')) == [1, 2, 3, 4]
697

  
698
    # change order
699
    app.get(
700
        '/manage/pricing/criteria/category/%s/order/' % (category.pk),
701
        params={
702
            'new-order': ','.join(str(x) for x in [criteria3.pk, criteria1.pk, criteria4.pk, criteria2.pk])
703
        },
704
    )
705
    assert list(category.criterias.values_list('pk', flat=True).order_by('order')) == [
706
        criteria3.pk,
707
        criteria1.pk,
708
        criteria4.pk,
709
        criteria2.pk,
710
    ]
711
    assert list(category.criterias.values_list('order', flat=True).order_by('order')) == [1, 2, 3, 4]
712

  
713

  
714
def test_reorder_criterias_as_manager(app, manager_user):
715
    category = CriteriaCategory.objects.create(label='QF')
716

  
717
    app = login(app, username='manager', password='manager')
718
    app.get('/manage/pricing/criteria/category/%s/order/' % (category.pk), status=403)
719

  
720

  
721
@pytest.mark.freeze_time('2021-07-08')
722
def test_import_criteria_category(app, admin_user):
723
    category = CriteriaCategory.objects.create(label='Foo bar')
724
    Criteria.objects.create(label='Foo', category=category)
725
    Criteria.objects.create(label='Baz', category=category)
726

  
727
    app = login(app)
728
    resp = app.get('/manage/pricing/criteria/category/%s/export/' % category.id)
729
    assert resp.headers['content-type'] == 'application/json'
730
    assert (
731
        resp.headers['content-disposition']
732
        == 'attachment; filename="export_pricing_category_foo-bar_20210708.json"'
733
    )
734
    category_export = resp.text
735

  
736
    # existing category
737
    resp = app.get('/manage/', status=200)
738
    resp = resp.click('Import')
739
    resp.form['agendas_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
740
    resp = resp.form.submit()
741
    assert resp.location.endswith('/manage/pricing/criterias/')
742
    resp = resp.follow()
743
    assert (
744
        'No pricing criteria category created. A pricing criteria category has been updated.' not in resp.text
745
    )
746
    assert CriteriaCategory.objects.count() == 1
747
    assert Criteria.objects.count() == 2
748

  
749
    # new category
750
    CriteriaCategory.objects.all().delete()
751
    resp = app.get('/manage/', status=200)
752
    resp = resp.click('Import')
753
    resp.form['agendas_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
754
    resp = resp.form.submit()
755
    assert resp.location.endswith('/manage/pricing/criterias/')
756
    resp = resp.follow()
757
    assert (
758
        'A pricing criteria category has been created. No pricing criteria category updated.' not in resp.text
759
    )
760
    assert CriteriaCategory.objects.count() == 1
761
    assert Criteria.objects.count() == 2
762

  
763
    # multiple categories
764
    categories = json.loads(category_export)
765
    categories['pricing_categories'].append(copy.copy(categories['pricing_categories'][0]))
766
    categories['pricing_categories'].append(copy.copy(categories['pricing_categories'][0]))
767
    categories['pricing_categories'][1]['label'] = 'Foo bar 2'
768
    categories['pricing_categories'][1]['slug'] = 'foo-bar-2'
769
    categories['pricing_categories'][2]['label'] = 'Foo bar 3'
770
    categories['pricing_categories'][2]['slug'] = 'foo-bar-3'
771

  
772
    resp = app.get('/manage/', status=200)
773
    resp = resp.click('Import')
774
    resp.form['agendas_json'] = Upload(
775
        'export.json', json.dumps(categories).encode('utf-8'), 'application/json'
776
    )
777
    resp = resp.form.submit()
778
    assert resp.location.endswith('/manage/')
779
    resp = resp.follow()
780
    assert (
781
        '2 pricing criteria categories have been created. A pricing criteria category has been updated.'
782
        in resp.text
783
    )
784
    assert CriteriaCategory.objects.count() == 3
785
    assert Criteria.objects.count() == 6
786

  
787
    CriteriaCategory.objects.all().delete()
788
    resp = app.get('/manage/', status=200)
789
    resp = resp.click('Import')
790
    resp.form['agendas_json'] = Upload(
791
        'export.json', json.dumps(categories).encode('utf-8'), 'application/json'
792
    )
793
    resp = resp.form.submit().follow()
794
    assert (
795
        '3 pricing criteria categories have been created. No pricing criteria category updated.' in resp.text
796
    )
797
    assert CriteriaCategory.objects.count() == 3
798
    assert Criteria.objects.count() == 6
799

  
800

  
801
def test_add_agenda_pricing(settings, app, admin_user):
802
    agenda = Agenda.objects.create(label='Foo Bar')
803
    pricing = Pricing.objects.create(label='Model')
804

  
805
    app = login(app)
806
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
807
    resp = resp.click('New pricing')
808
    resp.form['pricing'] = pricing.pk
809
    resp.form['date_start'] = '2021-09-01'
810
    resp.form['date_end'] = '2021-09-01'
811
    resp = resp.form.submit()
812
    assert resp.context['form'].errors['date_end'] == ['End date must be greater than start date.']
813
    resp.form['date_end'] = '2022-09-01'
814
    resp = resp.form.submit()
815
    agenda_pricing = AgendaPricing.objects.latest('pk')
816
    assert resp.location.endswith('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
817
    assert agenda_pricing.pricing == pricing
818
    assert agenda_pricing.agenda == agenda
819
    assert agenda_pricing.date_start == datetime.date(2021, 9, 1)
820
    assert agenda_pricing.date_end == datetime.date(2022, 9, 1)
821

  
822
    resp = app.get('/manage/pricing/agenda/%s/pricing/add/' % agenda.pk)
823
    resp.form['pricing'] = pricing.pk
824
    resp.form['date_start'] = '2021-11-01'
825
    resp.form['date_end'] = '2022-11-01'
826
    resp = resp.form.submit()
827
    assert resp.context['form'].errors['__all__'] == ['Pricing overlaps existing pricings.']
828
    resp.form['date_start'] = '2022-09-01'
829
    resp.form['date_end'] = '2023-09-01'
830
    resp = resp.form.submit()
831
    agenda_pricing = AgendaPricing.objects.latest('pk')
832
    assert agenda_pricing.pricing == pricing
833
    assert agenda_pricing.agenda == agenda
834
    assert agenda_pricing.date_start == datetime.date(2022, 9, 1)
835
    assert agenda_pricing.date_end == datetime.date(2023, 9, 1)
836

  
837
    settings.CHRONO_ENABLE_PRICING = False
838
    resp = app.get('/manage/')
839
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
840
    assert 'New pricing' not in resp
841

  
842

  
843
def test_add_agenda_pricing_as_manager(app, manager_user, agenda_with_restrictions):
844
    app = login(app, username='manager', password='manager')
845
    app.get('/manage/pricing/agenda/%s/pricing/add/' % agenda_with_restrictions.pk, status=403)
846

  
847

  
848
def test_edit_agenda_pricing(app, admin_user):
849
    agenda = Agenda.objects.create(label='Foo Bar')
850
    pricing = Pricing.objects.create(label='Model')
851
    pricing2 = Pricing.objects.create(label='Model 2')
852
    agenda_pricing = AgendaPricing.objects.create(
853
        agenda=agenda,
854
        pricing=pricing,
855
        date_start=datetime.date(year=2021, month=9, day=1),
856
        date_end=datetime.date(year=2022, month=9, day=1),
857
    )
858
    agenda2 = Agenda.objects.create(label='Foo Bar')
859
    agenda_pricing2 = AgendaPricing.objects.create(
860
        agenda=agenda2,
861
        pricing=pricing,
862
        date_start=datetime.date(year=2021, month=9, day=1),
863
        date_end=datetime.date(year=2022, month=9, day=1),
864
    )
865
    AgendaPricing.objects.create(
866
        agenda=agenda,
867
        pricing=pricing,
868
        date_start=datetime.date(year=2022, month=9, day=1),
869
        date_end=datetime.date(year=2023, month=9, day=1),
870
    )
871

  
872
    app = login(app)
873
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
874
    resp = resp.click(href='/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda.pk, agenda_pricing.pk))
875
    resp.form['pricing'] = pricing2.pk
876
    resp.form['date_start'] = '2021-09-01'
877
    resp.form['date_end'] = '2021-09-01'
878
    resp = resp.form.submit()
879
    assert resp.context['form'].errors['date_end'] == ['End date must be greater than start date.']
880
    resp.form['date_start'] = '2021-11-01'
881
    resp.form['date_end'] = '2022-11-01'
882
    resp = resp.form.submit()
883
    assert resp.context['form'].errors['__all__'] == ['Pricing overlaps existing pricings.']
884
    resp.form['date_start'] = '2021-08-01'
885
    resp.form['date_end'] = '2022-09-01'
886
    resp = resp.form.submit()
887
    assert resp.location.endswith('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
888
    agenda_pricing.refresh_from_db()
889
    assert agenda_pricing.pricing == pricing2
890
    assert agenda_pricing.agenda == agenda
891
    assert agenda_pricing.date_start == datetime.date(2021, 8, 1)
892
    assert agenda_pricing.date_end == datetime.date(2022, 9, 1)
893

  
894
    app.get('/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda.pk, agenda_pricing2.pk), status=404)
895
    app.get('/manage/pricing/agenda/%s/pricing/%s/edit/' % (0, agenda_pricing.pk), status=404)
896
    app.get('/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda.pk, 0), status=404)
897
    # wrong kind
898
    for kind in ['meetings', 'virtual']:
899
        agenda.kind = kind
900
        agenda.save()
901
        app.get('/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda.pk, agenda_pricing.pk), status=404)
902

  
903

  
904
def test_edit_agenda_pricing_as_manager(app, manager_user, agenda_with_restrictions):
905
    pricing = Pricing.objects.create(label='Model')
906
    agenda_pricing = AgendaPricing.objects.create(
907
        agenda=agenda_with_restrictions,
908
        pricing=pricing,
909
        date_start=datetime.date(year=2021, month=9, day=1),
910
        date_end=datetime.date(year=2021, month=10, day=1),
911
    )
912
    app = login(app, username='manager', password='manager')
913
    app.get(
914
        '/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda_with_restrictions.pk, agenda_pricing.pk),
915
        status=403,
916
    )
917

  
918
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda_with_restrictions.pk, agenda_pricing.pk))
919
    assert (
920
        '/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda_with_restrictions.pk, agenda_pricing.pk)
921
        not in resp
922
    )
923

  
924

  
925
def test_delete_agenda_pricing_as_manager(app, manager_user, agenda_with_restrictions):
926
    pricing = Pricing.objects.create(label='Model')
927
    agenda_pricing = AgendaPricing.objects.create(
928
        agenda=agenda_with_restrictions,
929
        pricing=pricing,
930
        date_start=datetime.date(year=2021, month=9, day=1),
931
        date_end=datetime.date(year=2021, month=10, day=1),
932
    )
933
    app = login(app, username='manager', password='manager')
934
    app.get(
935
        '/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda_with_restrictions.pk, agenda_pricing.pk),
936
        status=403,
937
    )
938

  
939
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda_with_restrictions.pk, agenda_pricing.pk))
940
    assert (
941
        '/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda_with_restrictions.pk, agenda_pricing.pk)
942
        not in resp
943
    )
944

  
945

  
946
def test_delete_agenda_pricing(app, admin_user):
947
    agenda = Agenda.objects.create(label='Foo Bar')
948
    pricing = Pricing.objects.create(label='Model')
949
    agenda_pricing = AgendaPricing.objects.create(
950
        agenda=agenda,
951
        pricing=pricing,
952
        date_start=datetime.date(year=2021, month=9, day=1),
953
        date_end=datetime.date(year=2022, month=9, day=1),
954
    )
955
    agenda2 = Agenda.objects.create(label='Foo Bar')
956
    agenda_pricing2 = AgendaPricing.objects.create(
957
        agenda=agenda2,
958
        pricing=pricing,
959
        date_start=datetime.date(year=2021, month=9, day=1),
960
        date_end=datetime.date(year=2022, month=9, day=1),
961
    )
962

  
963
    app = login(app)
964
    app.get('/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda.pk, agenda_pricing2.pk), status=404)
965
    app.get('/manage/pricing/agenda/%s/pricing/%s/delete/' % (0, agenda_pricing.pk), status=404)
966
    app.get('/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda.pk, 0), status=404)
967
    # wrong kind
968
    for kind in ['meetings', 'virtual']:
969
        agenda.kind = kind
970
        agenda.save()
971
        app.get('/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda.pk, agenda_pricing.pk), status=404)
972

  
973
    agenda.kind = 'events'
974
    agenda.save()
975
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
976
    resp = resp.click(href='/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda.pk, agenda_pricing.pk))
977
    resp = resp.form.submit()
978
    assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.pk)
979
    assert AgendaPricing.objects.filter(pk=agenda_pricing.pk).exists() is False
980

  
981

  
982
def test_detail_agenda_pricing(app, admin_user):
983
    agenda = Agenda.objects.create(label='Foo Bar')
984
    pricing = Pricing.objects.create(label='Model')
985
    agenda_pricing = AgendaPricing.objects.create(
986
        agenda=agenda,
987
        pricing=pricing,
988
        date_start=datetime.date(year=2021, month=9, day=1),
989
        date_end=datetime.date(year=2021, month=10, day=1),
990
    )
991
    agenda2 = Agenda.objects.create(label='Foo Bar')
992
    agenda_pricing2 = AgendaPricing.objects.create(
993
        agenda=agenda2,
994
        pricing=pricing,
995
        date_start=datetime.date(year=2021, month=9, day=1),
996
        date_end=datetime.date(year=2021, month=10, day=1),
997
    )
998

  
999
    app = login(app)
1000
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
1001
    resp = resp.click(href='/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1002

  
1003
    app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing2.pk), status=404)
1004
    app.get('/manage/pricing/agenda/%s/pricing/%s/' % (0, agenda_pricing.pk), status=404)
1005
    app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, 0), status=404)
1006
    # wrong kind
1007
    for kind in ['meetings', 'virtual']:
1008
        agenda.kind = kind
1009
        agenda.save()
1010
        app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk), status=404)
1011

  
1012

  
1013
def test_detail_agenda_pricing_3_categories(app, admin_user):
1014
    category1 = CriteriaCategory.objects.create(label='Cat 1')
1015
    Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)
1016
    Criteria.objects.create(label='Crit 1-2', slug='crit-1-2', category=category1, order=2)
1017
    category2 = CriteriaCategory.objects.create(label='Cat 2')
1018
    Criteria.objects.create(label='Crit 2-1', slug='crit-2-1', category=category2, order=1)
1019
    Criteria.objects.create(label='Crit 2-2', slug='crit-2-2', category=category2, order=2)
1020
    Criteria.objects.create(label='Crit 2-3', slug='crit-2-3', category=category2, order=3)
1021
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1022
    Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
1023
    Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
1024
    Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
1025
    Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
1026

  
1027
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1028
    pricing = Pricing.objects.create(label='Foo bar')
1029
    pricing.categories.add(category1, through_defaults={'order': 1})
1030
    pricing.categories.add(category2, through_defaults={'order': 2})
1031
    pricing.categories.add(category3, through_defaults={'order': 3})
1032
    pricing.criterias.set(Criteria.objects.all())
1033
    agenda_pricing = AgendaPricing.objects.create(
1034
        agenda=agenda,
1035
        pricing=pricing,
1036
        date_start=datetime.date(year=2021, month=9, day=1),
1037
        date_end=datetime.date(year=2021, month=10, day=1),
1038
        pricing_data={
1039
            'cat-1:crit-1-1': {
1040
                'cat-2:crit-2-1': {
1041
                    'cat-3:crit-3-1': 111,
1042
                    'cat-3:crit-3-3': 'not-a-decimal',
1043
                    'cat-3:crit-3-4': 114,
1044
                },
1045
                'cat-2:crit-2-3': {
1046
                    'cat-3:crit-3-2': 132,
1047
                },
1048
            },
1049
            'cat-1:crit-1-2': {
1050
                'cat-2:crit-2-2': {
1051
                    'cat-3:crit-3-3': 223,
1052
                },
1053
            },
1054
        },
1055
    )
1056

  
1057
    app = login(app)
1058
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1059
    assert '<h3>Crit 1-1</h3>' in resp
1060
    ths = resp.pyquery.find('table.pricing-matrix-crit-1-1 thead th')
1061
    assert len(ths) == 4
1062
    assert ths[0].text is None
1063
    assert ths[1].text == 'Crit 2-1'
1064
    assert ths[2].text == 'Crit 2-2'
1065
    assert ths[3].text == 'Crit 2-3'
1066
    assert resp.pyquery.find('table.pricing-matrix-crit-1-1 tr.pricing-row-crit-3-1 th')[0].text == 'Crit 3-1'
1067
    assert resp.pyquery.find('table.pricing-matrix-crit-1-1 tr.pricing-row-crit-3-2 th')[0].text == 'Crit 3-2'
1068
    assert resp.pyquery.find('table.pricing-matrix-crit-1-1 tr.pricing-row-crit-3-3 th')[0].text == 'Crit 3-3'
1069
    assert resp.pyquery.find('table.pricing-matrix-crit-1-1 tr.pricing-row-crit-3-4 th')[0].text == 'Crit 3-4'
1070
    assert (
1071
        resp.pyquery.find('table.pricing-matrix-crit-1-1 tr.pricing-row-crit-3-1 td.pricing-cell-crit-2-1')[
1072
            0
1073
        ].text
1074
        == '111.00'
1075
    )
1076
    assert (
1077
        resp.pyquery.find('table.pricing-matrix-crit-1-1 tr.pricing-row-crit-3-2 td.pricing-cell-crit-2-1')[
1078
            0
1079
        ].text
1080
        is None
1081
    )  # not defined
1082
    assert (
1083
        resp.pyquery.find('table.pricing-matrix-crit-1-1 tr.pricing-row-crit-3-3 td.pricing-cell-crit-2-1')[
1084
            0
1085
        ].text
1086
        is None
1087
    )  # wrong value
1088
    assert (
1089
        resp.pyquery.find('table.pricing-matrix-crit-1-1 tr.pricing-row-crit-3-4 td.pricing-cell-crit-2-1')[
1090
            0
1091
        ].text
1092
        == '114.00'
1093
    )
1094
    assert (
1095
        resp.pyquery.find('table.pricing-matrix-crit-1-1 tr.pricing-row-crit-3-2 td.pricing-cell-crit-2-3')[
1096
            0
1097
        ].text
1098
        == '132.00'
1099
    )
1100
    assert '<h3>Crit 1-2</h3>' in resp
1101
    ths = resp.pyquery.find('table.pricing-matrix-crit-1-2 thead th')
1102
    assert len(ths) == 4
1103
    assert ths[0].text is None
1104
    assert ths[1].text == 'Crit 2-1'
1105
    assert ths[2].text == 'Crit 2-2'
1106
    assert ths[3].text == 'Crit 2-3'
1107
    assert resp.pyquery.find('table.pricing-matrix-crit-1-2 tr.pricing-row-crit-3-1 th')[0].text == 'Crit 3-1'
1108
    assert resp.pyquery.find('table.pricing-matrix-crit-1-2 tr.pricing-row-crit-3-2 th')[0].text == 'Crit 3-2'
1109
    assert resp.pyquery.find('table.pricing-matrix-crit-1-2 tr.pricing-row-crit-3-3 th')[0].text == 'Crit 3-3'
1110
    assert resp.pyquery.find('table.pricing-matrix-crit-1-2 tr.pricing-row-crit-3-4 th')[0].text == 'Crit 3-4'
1111
    assert (
1112
        resp.pyquery.find('table.pricing-matrix-crit-1-2 tr.pricing-row-crit-3-2 td.pricing-cell-crit-2-2')[
1113
            0
1114
        ].text
1115
        is None
1116
    )  # not defined
1117
    assert (
1118
        resp.pyquery.find('table.pricing-matrix-crit-1-2 tr.pricing-row-crit-3-3 td.pricing-cell-crit-2-2')[
1119
            0
1120
        ].text
1121
        == '223.00'
1122
    )
1123

  
1124

  
1125
def test_detail_agenda_pricing_2_categories(app, admin_user):
1126
    category2 = CriteriaCategory.objects.create(label='Cat 2')
1127
    Criteria.objects.create(label='Crit 2-1', slug='crit-2-1', category=category2, order=1)
1128
    Criteria.objects.create(label='Crit 2-2', slug='crit-2-2', category=category2, order=2)
1129
    Criteria.objects.create(label='Crit 2-3', slug='crit-2-3', category=category2, order=3)
1130
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1131
    Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
1132
    Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
1133
    Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
1134
    Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
1135

  
1136
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1137
    pricing = Pricing.objects.create(label='Foo bar')
1138
    pricing.categories.add(category2, through_defaults={'order': 2})
1139
    pricing.categories.add(category3, through_defaults={'order': 3})
1140
    pricing.criterias.set(Criteria.objects.all())
1141
    agenda_pricing = AgendaPricing.objects.create(
1142
        agenda=agenda,
1143
        pricing=pricing,
1144
        date_start=datetime.date(year=2021, month=9, day=1),
1145
        date_end=datetime.date(year=2021, month=10, day=1),
1146
        pricing_data={
1147
            'cat-2:crit-2-1': {
1148
                'cat-3:crit-3-1': 111,
1149
                'cat-3:crit-3-3': 'not-a-decimal',
1150
                'cat-3:crit-3-4': 114,
1151
            },
1152
            'cat-2:crit-2-3': {
1153
                'cat-3:crit-3-2': 132,
1154
            },
1155
        },
1156
    )
1157

  
1158
    app = login(app)
1159
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1160
    assert '<h3>' not in resp
1161
    ths = resp.pyquery.find('table thead th')
1162
    assert len(ths) == 4
1163
    assert ths[0].text is None
1164
    assert ths[1].text == 'Crit 2-1'
1165
    assert ths[2].text == 'Crit 2-2'
1166
    assert ths[3].text == 'Crit 2-3'
1167
    assert resp.pyquery.find('table tr.pricing-row-crit-3-1 th')[0].text == 'Crit 3-1'
1168
    assert resp.pyquery.find('table tr.pricing-row-crit-3-2 th')[0].text == 'Crit 3-2'
1169
    assert resp.pyquery.find('table tr.pricing-row-crit-3-3 th')[0].text == 'Crit 3-3'
1170
    assert resp.pyquery.find('table tr.pricing-row-crit-3-4 th')[0].text == 'Crit 3-4'
1171
    assert resp.pyquery.find('table tr.pricing-row-crit-3-1 td.pricing-cell-crit-2-1')[0].text == '111.00'
1172
    assert (
1173
        resp.pyquery.find('table tr.pricing-row-crit-3-2 td.pricing-cell-crit-2-1')[0].text is None
1174
    )  # not defined
1175
    assert (
1176
        resp.pyquery.find('table tr.pricing-row-crit-3-3 td.pricing-cell-crit-2-1')[0].text is None
1177
    )  # wrong value
1178
    assert resp.pyquery.find('table tr.pricing-row-crit-3-4 td.pricing-cell-crit-2-1')[0].text == '114.00'
1179
    assert resp.pyquery.find('table tr.pricing-row-crit-3-2 td.pricing-cell-crit-2-3')[0].text == '132.00'
1180

  
1181

  
1182
def test_detail_agenda_pricing_1_category(app, admin_user):
1183
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1184
    Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
1185
    Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
1186
    Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
1187
    Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
1188

  
1189
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1190
    pricing = Pricing.objects.create(label='Foo bar')
1191
    pricing.categories.add(category3, through_defaults={'order': 3})
1192
    pricing.criterias.set(Criteria.objects.all())
1193
    agenda_pricing = AgendaPricing.objects.create(
1194
        agenda=agenda,
1195
        pricing=pricing,
1196
        date_start=datetime.date(year=2021, month=9, day=1),
1197
        date_end=datetime.date(year=2021, month=10, day=1),
1198
        pricing_data={
1199
            'cat-3:crit-3-1': 111,
1200
            'cat-3:crit-3-3': 'not-a-decimal',
1201
            'cat-3:crit-3-4': 114,
1202
        },
1203
    )
1204

  
1205
    app = login(app)
1206
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1207
    assert '<h3>' not in resp
1208
    ths = resp.pyquery.find('table thead')
1209
    assert len(ths) == 0
1210
    assert resp.pyquery.find('table tr.pricing-row-crit-3-1 th')[0].text == 'Crit 3-1'
1211
    assert resp.pyquery.find('table tr.pricing-row-crit-3-2 th')[0].text == 'Crit 3-2'
1212
    assert resp.pyquery.find('table tr.pricing-row-crit-3-3 th')[0].text == 'Crit 3-3'
1213
    assert resp.pyquery.find('table tr.pricing-row-crit-3-4 th')[0].text == 'Crit 3-4'
1214
    assert resp.pyquery.find('table tr.pricing-row-crit-3-1 td')[0].text == '111.00'
1215
    assert resp.pyquery.find('table tr.pricing-row-crit-3-2 td')[0].text is None  # not defined
1216
    assert resp.pyquery.find('table tr.pricing-row-crit-3-3 td')[0].text is None  # wrong value
1217
    assert resp.pyquery.find('table tr.pricing-row-crit-3-4 td')[0].text == '114.00'
1218

  
1219

  
1220
def test_edit_agenda_pricing_matrix_3_categories(app, admin_user):
1221
    category1 = CriteriaCategory.objects.create(label='Cat 1')
1222
    criteria11 = Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)
1223
    criteria12 = Criteria.objects.create(label='Crit 1-2', slug='crit-1-2', category=category1, order=2)
1224
    category2 = CriteriaCategory.objects.create(label='Cat 2')
1225
    criteria21 = Criteria.objects.create(label='Crit 2-1', slug='crit-2-1', category=category2, order=1)
1226
    Criteria.objects.create(label='Crit 2-2', slug='crit-2-2', category=category2, order=2)
1227
    Criteria.objects.create(label='Crit 2-3', slug='crit-2-3', category=category2, order=3)
1228
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1229
    criteria31 = Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
1230
    Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
1231
    Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
1232
    Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
1233

  
1234
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1235
    pricing = Pricing.objects.create(label='Foo bar')
1236
    pricing.categories.add(category1, through_defaults={'order': 1})
1237
    pricing.categories.add(category2, through_defaults={'order': 2})
1238
    pricing.categories.add(category3, through_defaults={'order': 3})
1239
    pricing.criterias.set(Criteria.objects.all())
1240
    agenda_pricing = AgendaPricing.objects.create(
1241
        agenda=agenda,
1242
        pricing=pricing,
1243
        date_start=datetime.date(year=2021, month=9, day=1),
1244
        date_end=datetime.date(year=2021, month=10, day=1),
1245
        pricing_data={
1246
            'cat-1:crit-1-1': {
1247
                'cat-2:crit-2-1': {
1248
                    'cat-3:crit-3-1': 111,
1249
                    'cat-3:crit-3-3': 'not-a-decimal',
1250
                    'cat-3:crit-3-4': 114,
1251
                },
1252
                'cat-2:crit-2-3': {
1253
                    'cat-3:crit-3-2': 132,
1254
                },
1255
            },
1256
            'cat-1:crit-1-2': {
1257
                'cat-2:crit-2-2': {
1258
                    'cat-3:crit-3-3': 223,
1259
                },
1260
            },
1261
        },
1262
    )
1263

  
1264
    app = login(app)
1265
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1266
    resp = resp.click(
1267
        href='/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1268
        % (agenda.pk, agenda_pricing.pk, criteria11.slug)
1269
    )
1270
    assert resp.form['form-0-crit_0'].value == '111'
1271
    assert resp.form['form-0-crit_1'].value == ''
1272
    assert resp.form['form-0-crit_2'].value == ''
1273
    assert resp.form['form-1-crit_0'].value == ''
1274
    assert resp.form['form-1-crit_1'].value == ''
1275
    assert resp.form['form-1-crit_2'].value == '132'
1276
    assert resp.form['form-2-crit_0'].value == ''
1277
    assert resp.form['form-2-crit_1'].value == ''
1278
    assert resp.form['form-2-crit_2'].value == ''
1279
    assert resp.form['form-3-crit_0'].value == '114'
1280
    assert resp.form['form-3-crit_1'].value == ''
1281
    assert resp.form['form-3-crit_2'].value == ''
1282
    resp.form['form-0-crit_1'] = '121'
1283
    resp.form['form-0-crit_2'] = '131'
1284
    resp.form['form-1-crit_0'] = '112'
1285
    resp.form['form-1-crit_1'] = '122'
1286
    resp.form['form-1-crit_2'] = '132.5'
1287
    resp.form['form-2-crit_0'] = '113'
1288
    resp.form['form-2-crit_1'] = '123'
1289
    resp.form['form-2-crit_2'] = '133'
1290
    resp.form['form-3-crit_0'] = '914'
1291
    resp.form['form-3-crit_1'] = '124'
1292
    resp.form['form-3-crit_2'] = '134'
1293
    resp = resp.form.submit()
1294
    assert resp.location.endswith('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1295
    agenda_pricing.refresh_from_db()
1296
    assert agenda_pricing.pricing_data == {
1297
        'cat-1:crit-1-1': {
1298
            'cat-2:crit-2-1': {
1299
                'cat-3:crit-3-1': 111,
1300
                'cat-3:crit-3-2': 112,
1301
                'cat-3:crit-3-3': 113,
1302
                'cat-3:crit-3-4': 914,
1303
            },
1304
            'cat-2:crit-2-2': {
1305
                'cat-3:crit-3-1': 121,
1306
                'cat-3:crit-3-2': 122,
1307
                'cat-3:crit-3-3': 123,
1308
                'cat-3:crit-3-4': 124,
1309
            },
1310
            'cat-2:crit-2-3': {
1311
                'cat-3:crit-3-1': 131,
1312
                'cat-3:crit-3-2': 132.5,
1313
                'cat-3:crit-3-3': 133,
1314
                'cat-3:crit-3-4': 134,
1315
            },
1316
        },
1317
        'cat-1:crit-1-2': {
1318
            'cat-2:crit-2-2': {
1319
                'cat-3:crit-3-3': 223,
1320
            },
1321
        },
1322
    }
1323
    app.get(
1324
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1325
        % (agenda.pk, agenda_pricing.pk, criteria12.slug),
1326
        status=200,
1327
    )
1328

  
1329
    agenda2 = Agenda.objects.create(label='Foo Bar')
1330
    agenda_pricing2 = AgendaPricing.objects.create(
1331
        agenda=agenda2,
1332
        pricing=pricing,
1333
        date_start=datetime.date(year=2021, month=9, day=1),
1334
        date_end=datetime.date(year=2022, month=9, day=1),
1335
    )
1336
    app.get(
1337
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1338
        % (agenda.pk, agenda_pricing2.pk, criteria11.slug),
1339
        status=404,
1340
    )
1341
    app.get(
1342
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/' % (0, agenda_pricing.pk, criteria11.slug),
1343
        status=404,
1344
    )
1345
    app.get(
1346
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/' % (agenda.pk, 0, criteria11.slug), status=404
1347
    )
1348
    app.get(
1349
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/' % (agenda.pk, agenda_pricing.pk, 'unknown'),
1350
        status=404,
1351
    )
1352
    app.get(
1353
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1354
        % (agenda.pk, agenda_pricing.pk, criteria21.slug),
1355
        status=404,
1356
    )
1357
    app.get(
1358
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1359
        % (agenda.pk, agenda_pricing.pk, criteria31.slug),
1360
        status=404,
1361
    )
1362
    app.get('/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, agenda_pricing.pk), status=404)
1363
    # wrong kind
1364
    for kind in ['meetings', 'virtual']:
1365
        agenda.kind = kind
1366
        agenda.save()
1367
        app.get(
1368
            '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1369
            % (agenda.pk, agenda_pricing.pk, criteria11.slug),
1370
            status=404,
1371
        )
1372

  
1373

  
1374
def test_edit_agenda_pricing_matrix_2_categories(app, admin_user):
1375
    category2 = CriteriaCategory.objects.create(label='Cat 2')
1376
    criteria21 = Criteria.objects.create(label='Crit 2-1', slug='crit-2-1', category=category2, order=1)
1377
    Criteria.objects.create(label='Crit 2-2', slug='crit-2-2', category=category2, order=2)
1378
    Criteria.objects.create(label='Crit 2-3', slug='crit-2-3', category=category2, order=3)
1379
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1380
    criteria31 = Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
1381
    Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
1382
    Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
1383
    Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
1384

  
1385
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1386
    pricing = Pricing.objects.create(label='Foo bar')
1387
    pricing.categories.add(category2, through_defaults={'order': 2})
1388
    pricing.categories.add(category3, through_defaults={'order': 3})
1389
    pricing.criterias.set(Criteria.objects.all())
1390
    agenda_pricing = AgendaPricing.objects.create(
1391
        agenda=agenda,
1392
        pricing=pricing,
1393
        date_start=datetime.date(year=2021, month=9, day=1),
1394
        date_end=datetime.date(year=2021, month=10, day=1),
1395
        pricing_data={
1396
            'cat-2:crit-2-1': {
1397
                'cat-3:crit-3-1': 111,
1398
                'cat-3:crit-3-3': 'not-a-decimal',
1399
                'cat-3:crit-3-4': 114,
1400
            },
1401
            'cat-2:crit-2-3': {
1402
                'cat-3:crit-3-2': 132,
1403
            },
1404
        },
1405
    )
1406

  
1407
    app = login(app)
1408
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1409
    resp = resp.click(
1410
        href='/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, agenda_pricing.pk)
1411
    )
1412
    assert resp.form['form-0-crit_0'].value == '111'
1413
    assert resp.form['form-0-crit_1'].value == ''
1414
    assert resp.form['form-0-crit_2'].value == ''
1415
    assert resp.form['form-1-crit_0'].value == ''
1416
    assert resp.form['form-1-crit_1'].value == ''
1417
    assert resp.form['form-1-crit_2'].value == '132'
1418
    assert resp.form['form-2-crit_0'].value == ''
1419
    assert resp.form['form-2-crit_1'].value == ''
1420
    assert resp.form['form-2-crit_2'].value == ''
1421
    assert resp.form['form-3-crit_0'].value == '114'
1422
    assert resp.form['form-3-crit_1'].value == ''
1423
    assert resp.form['form-3-crit_2'].value == ''
1424
    resp.form['form-0-crit_1'] = '121'
1425
    resp.form['form-0-crit_2'] = '131'
1426
    resp.form['form-1-crit_0'] = '112'
1427
    resp.form['form-1-crit_1'] = '122'
1428
    resp.form['form-1-crit_2'] = '132.5'
1429
    resp.form['form-2-crit_0'] = '113'
1430
    resp.form['form-2-crit_1'] = '123'
1431
    resp.form['form-2-crit_2'] = '133'
1432
    resp.form['form-3-crit_0'] = '914'
1433
    resp.form['form-3-crit_1'] = '124'
1434
    resp.form['form-3-crit_2'] = '134'
1435
    resp = resp.form.submit()
1436
    assert resp.location.endswith('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1437
    agenda_pricing.refresh_from_db()
1438
    assert agenda_pricing.pricing_data == {
1439
        'cat-2:crit-2-1': {
1440
            'cat-3:crit-3-1': 111,
1441
            'cat-3:crit-3-2': 112,
1442
            'cat-3:crit-3-3': 113,
1443
            'cat-3:crit-3-4': 914,
1444
        },
1445
        'cat-2:crit-2-2': {
1446
            'cat-3:crit-3-1': 121,
1447
            'cat-3:crit-3-2': 122,
1448
            'cat-3:crit-3-3': 123,
1449
            'cat-3:crit-3-4': 124,
1450
        },
1451
        'cat-2:crit-2-3': {
1452
            'cat-3:crit-3-1': 131,
1453
            'cat-3:crit-3-2': 132.5,
1454
            'cat-3:crit-3-3': 133,
1455
            'cat-3:crit-3-4': 134,
1456
        },
1457
    }
1458

  
1459
    agenda2 = Agenda.objects.create(label='Foo Bar')
1460
    agenda_pricing2 = AgendaPricing.objects.create(
1461
        agenda=agenda2,
1462
        pricing=pricing,
1463
        date_start=datetime.date(year=2021, month=9, day=1),
1464
        date_end=datetime.date(year=2022, month=9, day=1),
1465
    )
1466
    app.get('/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, agenda_pricing2.pk), status=404)
1467
    app.get('/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (0, agenda_pricing.pk), status=404)
1468
    app.get('/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, 0), status=404)
1469
    app.get(
1470
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1471
        % (agenda.pk, agenda_pricing.pk, criteria21.slug),
1472
        status=404,
1473
    )
1474
    app.get(
1475
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1476
        % (agenda.pk, agenda_pricing.pk, criteria31.slug),
1477
        status=404,
1478
    )
1479
    # wrong kind
1480
    for kind in ['meetings', 'virtual']:
1481
        agenda.kind = kind
1482
        agenda.save()
1483
        app.get(
1484
            '/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, agenda_pricing.pk), status=404
1485
        )
1486

  
1487

  
1488
def test_edit_agenda_pricing_matrix_1_category(app, admin_user):
1489
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1490
    criteria31 = Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
1491
    Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
1492
    Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
1493
    Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
1494

  
1495
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1496
    pricing = Pricing.objects.create(label='Foo bar')
1497
    pricing.categories.add(category3, through_defaults={'order': 3})
1498
    pricing.criterias.set(Criteria.objects.all())
1499
    agenda_pricing = AgendaPricing.objects.create(
1500
        agenda=agenda,
1501
        pricing=pricing,
1502
        date_start=datetime.date(year=2021, month=9, day=1),
1503
        date_end=datetime.date(year=2021, month=10, day=1),
1504
        pricing_data={
1505
            'cat-3:crit-3-1': 111,
1506
            'cat-3:crit-3-3': 'not-a-decimal',
1507
            'cat-3:crit-3-4': 114,
1508
        },
1509
    )
1510

  
1511
    app = login(app)
1512
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1513
    resp = resp.click(
1514
        href='/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, agenda_pricing.pk)
1515
    )
1516
    assert resp.form['form-0-crit_0'].value == '111'
1517
    assert resp.form['form-1-crit_0'].value == ''
1518
    assert resp.form['form-2-crit_0'].value == ''
1519
    assert resp.form['form-3-crit_0'].value == '114'
1520
    resp.form['form-1-crit_0'] = '112.5'
1521
    resp.form['form-2-crit_0'] = '113'
1522
    resp.form['form-3-crit_0'] = '914'
1523
    resp = resp.form.submit()
1524
    assert resp.location.endswith('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
1525
    agenda_pricing.refresh_from_db()
1526
    assert agenda_pricing.pricing_data == {
1527
        'cat-3:crit-3-1': 111,
1528
        'cat-3:crit-3-2': 112.5,
1529
        'cat-3:crit-3-3': 113,
1530
        'cat-3:crit-3-4': 914,
1531
    }
1532

  
1533
    agenda2 = Agenda.objects.create(label='Foo Bar')
1534
    agenda_pricing2 = AgendaPricing.objects.create(
1535
        agenda=agenda2,
1536
        pricing=pricing,
1537
        date_start=datetime.date(year=2021, month=9, day=1),
1538
        date_end=datetime.date(year=2022, month=9, day=1),
1539
    )
1540
    app.get('/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, agenda_pricing2.pk), status=404)
1541
    app.get('/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (0, agenda_pricing.pk), status=404)
1542
    app.get('/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, 0), status=404)
1543
    app.get(
1544
        '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1545
        % (agenda.pk, agenda_pricing.pk, criteria31.slug),
1546
        status=404,
1547
    )
1548
    # wrong kind
1549
    for kind in ['meetings', 'virtual']:
1550
        agenda.kind = kind
1551
        agenda.save()
1552
        app.get(
1553
            '/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, agenda_pricing.pk), status=404
1554
        )
1555

  
1556

  
1557
def test_edit_agenda_pricing_matrix_empty(app, admin_user):
1558
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1559
    pricing = Pricing.objects.create(label='Foo bar')
1560
    agenda_pricing = AgendaPricing.objects.create(
1561
        agenda=agenda,
1562
        pricing=pricing,
1563
        date_start=datetime.date(year=2021, month=9, day=1),
1564
        date_end=datetime.date(year=2021, month=10, day=1),
1565
    )
1566

  
1567
    app = login(app)
1568
    app.get('/manage/pricing/agenda/%s/pricing/%s/matrix/edit/' % (agenda.pk, agenda_pricing.pk), status=404)
1569

  
1570

  
1571
def test_edit_agenda_pricing_matrix_as_manager(app, manager_user, agenda_with_restrictions):
1572
    category1 = CriteriaCategory.objects.create(label='Cat 1')
1573
    Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)
1574
    Criteria.objects.create(label='Crit 1-2', slug='crit-1-2', category=category1, order=2)
1575
    category2 = CriteriaCategory.objects.create(label='Cat 2')
1576
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1577
    pricing = Pricing.objects.create(label='Model')
1578
    agenda_pricing = AgendaPricing.objects.create(
1579
        agenda=agenda_with_restrictions,
1580
        pricing=pricing,
1581
        date_start=datetime.date(year=2021, month=9, day=1),
1582
        date_end=datetime.date(year=2021, month=10, day=1),
1583
    )
1584
    pricing.categories.add(category1, through_defaults={'order': 1})
1585
    pricing.criterias.set(Criteria.objects.all())
1586

  
1587
    def check():
1588
        resp = app.get(
1589
            '/manage/pricing/agenda/%s/pricing/%s/' % (agenda_with_restrictions.pk, agenda_pricing.pk)
1590
        )
1591
        assert (
1592
            '/manage/pricing/agenda/%s/pricing/%s/matrix/edit/'
1593
            % (agenda_with_restrictions.pk, agenda_pricing.pk)
1594
            not in resp
1595
        )
1596
        for slug in ['crit-1-1', 'crit-1-2']:
1597
            assert (
1598
                '/manage/pricing/agenda/%s/pricing/%s/matrix/%s/edit/'
1599
                % (agenda_with_restrictions.pk, agenda_pricing.pk, slug)
1600
                not in resp
1601
            )
1602

  
1603
    app = login(app, username='manager', password='manager')
1604
    app.get(
1605
        '/manage/pricing/agenda/%s/pricing/%s/matrix/edit/'
1606
        % (agenda_with_restrictions.pk, agenda_pricing.pk),
1607
        status=403,
1608
    )
1609
    check()
1610

  
1611
    pricing.categories.add(category2, through_defaults={'order': 2})
1612
    app.get(
1613
        '/manage/pricing/agenda/%s/pricing/%s/matrix/edit/'
1614
        % (agenda_with_restrictions.pk, agenda_pricing.pk),
1615
        status=403,
1616
    )
1617
    check()
1618

  
1619
    pricing.categories.add(category3, through_defaults={'order': 3})
1620
    app.get(
1621
        '/manage/pricing/agenda/%s/pricing/%s/matrix/crit-1-1/edit/'
1622
        % (agenda_with_restrictions.pk, agenda_pricing.pk),
1623
        status=403,
1624
    )
1625
    app.get(
1626
        '/manage/pricing/agenda/%s/pricing/%s/matrix/crit-1-2/edit/'
1627
        % (agenda_with_restrictions.pk, agenda_pricing.pk),
1628
        status=403,
1629
    )
1630
    check()
tests/pricing/test_models.py
1
import datetime
2
import json
3
from unittest import mock
4

  
5
import pytest
6
from django.template import Context
7
from django.test.client import RequestFactory
8
from django.utils.timezone import make_aware, now
9
from publik_django_templatetags.wcs.context_processors import Cards
10

  
11
from chrono.agendas.models import Agenda, Booking, CheckType, CheckTypeGroup, Event, Subscription
12
from chrono.pricing.models import (
13
    AgendaPricing,
14
    AgendaPricingNotFound,
15
    Criteria,
16
    CriteriaCategory,
17
    CriteriaConditionNotFound,
18
    Pricing,
19
    PricingBookingCheckTypeError,
20
    PricingBookingNotCheckedError,
21
    PricingCriteriaCategory,
22
    PricingDataError,
23
    PricingDataFormatError,
24
    PricingEventNotCheckedError,
25
    PricingMatrix,
26
    PricingMatrixCell,
27
    PricingMatrixRow,
28
    PricingMultipleBookingError,
29
    PricingSubscriptionError,
30
)
31

  
32
pytestmark = pytest.mark.django_db
33

  
34

  
35
@pytest.fixture
36
def context():
37
    return Context(
38
        {
39
            'cards': Cards(),
40
            'request': RequestFactory().get('/'),
41
        }
42
    )
43

  
44

  
45
class MockedRequestResponse(mock.Mock):
46
    status_code = 200
47

  
48
    def json(self):
49
        return json.loads(self.content)
50

  
51

  
52
def mocked_requests_send(request, **kwargs):
53
    data = [{'id': 1, 'fields': {'foo': 'bar'}}, {'id': 2, 'fields': {'foo': 'baz'}}]  # fake result
54
    return MockedRequestResponse(content=json.dumps({'data': data}))
55

  
56

  
57
def test_criteria_category_slug():
58
    category = CriteriaCategory.objects.create(label='Foo bar')
59
    assert category.slug == 'foo-bar'
60

  
61

  
62
def test_criteria_category_existing_slug():
63
    category = CriteriaCategory.objects.create(label='Foo bar', slug='bar')
64
    assert category.slug == 'bar'
65

  
66

  
67
def test_criteria_category_duplicate_slugs():
68
    category = CriteriaCategory.objects.create(label='Foo baz')
69
    assert category.slug == 'foo-baz'
70
    category = CriteriaCategory.objects.create(label='Foo baz')
71
    assert category.slug == 'foo-baz-1'
72
    category = CriteriaCategory.objects.create(label='Foo baz')
73
    assert category.slug == 'foo-baz-2'
74

  
75

  
76
def test_criteria_slug():
77
    category = CriteriaCategory.objects.create(label='Foo')
78
    criteria = Criteria.objects.create(label='Foo bar', category=category)
79
    assert criteria.slug == 'foo-bar'
80

  
81

  
82
def test_criteria_existing_slug():
83
    category = CriteriaCategory.objects.create(label='Foo')
84
    criteria = Criteria.objects.create(label='Foo bar', slug='bar', category=category)
85
    assert criteria.slug == 'bar'
86

  
87

  
88
def test_criteria_duplicate_slugs():
89
    category = CriteriaCategory.objects.create(label='Foo')
90
    category2 = CriteriaCategory.objects.create(label='Bar')
91
    Criteria.objects.create(label='Foo baz', slug='foo-baz', category=category2)
92
    criteria = Criteria.objects.create(label='Foo baz', category=category)
93
    assert criteria.slug == 'foo-baz'
94
    criteria = Criteria.objects.create(label='Foo baz', category=category)
95
    assert criteria.slug == 'foo-baz-1'
96
    criteria = Criteria.objects.create(label='Foo baz', category=category)
97
    assert criteria.slug == 'foo-baz-2'
98

  
99

  
100
def test_criteria_order():
101
    category = CriteriaCategory.objects.create(label='Foo')
102
    criteria = Criteria.objects.create(label='Foo bar', category=category)
103
    assert criteria.order == 1
104

  
105

  
106
def test_criteria_existing_order():
107
    category = CriteriaCategory.objects.create(label='Foo')
108
    criteria = Criteria.objects.create(label='Foo bar', order=42, category=category)
109
    assert criteria.order == 42
110

  
111

  
112
def test_criteria_duplicate_orders():
113
    category = CriteriaCategory.objects.create(label='Foo')
114
    category2 = CriteriaCategory.objects.create(label='Bar')
115
    Criteria.objects.create(label='Foo baz', order=1, category=category2)
116
    criteria = Criteria.objects.create(label='Foo baz', category=category)
117
    assert criteria.order == 1
118
    criteria = Criteria.objects.create(label='Foo baz', category=category)
119
    assert criteria.order == 2
120
    criteria = Criteria.objects.create(label='Foo baz', category=category)
121
    assert criteria.order == 3
122

  
123

  
124
def test_pricing_slug():
125
    pricing = Pricing.objects.create(label='Foo bar')
126
    assert pricing.slug == 'foo-bar'
127

  
128

  
129
def test_pricing_existing_slug():
130
    pricing = Pricing.objects.create(label='Foo bar', slug='bar')
131
    assert pricing.slug == 'bar'
132

  
133

  
134
def test_pricing_duplicate_slugs():
135
    pricing = Pricing.objects.create(label='Foo baz')
136
    assert pricing.slug == 'foo-baz'
137
    pricing = Pricing.objects.create(label='Foo baz')
138
    assert pricing.slug == 'foo-baz-1'
139
    pricing = Pricing.objects.create(label='Foo baz')
140
    assert pricing.slug == 'foo-baz-2'
141

  
142

  
143
def test_pricing_category_criteria_order():
144
    category = CriteriaCategory.objects.create(label='Foo')
145
    pricing = Pricing.objects.create(label='Foo bar')
146
    pcc = PricingCriteriaCategory.objects.create(pricing=pricing, category=category)
147
    assert pcc.order == 1
148

  
149

  
150
def test_pricing_category_criteria_existing_order():
151
    category = CriteriaCategory.objects.create(label='Foo')
152
    pricing = Pricing.objects.create(label='Foo bar')
153
    pcc = PricingCriteriaCategory.objects.create(order=42, pricing=pricing, category=category)
154
    assert pcc.order == 42
155

  
156

  
157
def test_pricing_category_criteria_duplicate_orders():
158
    category1 = CriteriaCategory.objects.create(label='Foo')
159
    category2 = CriteriaCategory.objects.create(label='Bar')
160
    category3 = CriteriaCategory.objects.create(label='Baz')
161
    pricing = Pricing.objects.create(label='Foo bar')
162
    pricing2 = Pricing.objects.create(label='Foo baz')
163
    PricingCriteriaCategory.objects.create(order=1, pricing=pricing2, category=category1)
164
    PricingCriteriaCategory.objects.create(order=2, pricing=pricing2, category=category2)
165
    PricingCriteriaCategory.objects.create(order=3, pricing=pricing2, category=category3)
166
    pcc = PricingCriteriaCategory.objects.create(pricing=pricing, category=category1)
167
    assert pcc.order == 1
168
    pcc = PricingCriteriaCategory.objects.create(pricing=pricing, category=category2)
169
    assert pcc.order == 2
170
    pcc = PricingCriteriaCategory.objects.create(pricing=pricing, category=category3)
171
    assert pcc.order == 3
172

  
173

  
174
def test_pricing_duplicate():
175
    category1 = CriteriaCategory.objects.create(label='Cat 1')
176
    Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)
177
    Criteria.objects.create(label='Crit 1-2', slug='crit-1-2', category=category1, order=2)
178
    category2 = CriteriaCategory.objects.create(label='Cat 2')
179
    Criteria.objects.create(label='Crit 2-1', slug='crit-2-1', category=category2, order=1)
180
    Criteria.objects.create(label='Crit 2-2', slug='crit-2-2', category=category2, order=2)
181
    Criteria.objects.create(label='Crit 2-3', slug='crit-2-3', category=category2, order=3)
182
    category3 = CriteriaCategory.objects.create(label='Cat 3')
183
    Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
184
    Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
185
    Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
186
    Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
187
    not_used = Criteria.objects.create(label='Not used', slug='crit-3-notused', category=category3, order=5)
188

  
189
    pricing = Pricing.objects.create(
190
        label='Foo',
191
        extra_variables={
192
            'foo': 'bar',
193
        },
194
    )
195
    pricing.categories.add(category1, through_defaults={'order': 1})
196
    pricing.categories.add(category2, through_defaults={'order': 2})
197
    pricing.categories.add(category3, through_defaults={'order': 3})
198
    pricing.criterias.set(Criteria.objects.exclude(pk=not_used.pk))
199

  
200
    new_pricing = pricing.duplicate()
201
    assert new_pricing.label == 'Copy of Foo'
202
    assert new_pricing.slug == 'copy-of-foo'
203
    assert new_pricing.extra_variables == pricing.extra_variables
204
    assert list(new_pricing.criterias.all()) == list(pricing.criterias.all())
205
    original_pcc_categories = list(
206
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
207
    )
208
    new_pcc_categories = list(
209
        PricingCriteriaCategory.objects.filter(pricing=new_pricing).values_list('category', flat=True)
210
    )
211
    assert new_pcc_categories == original_pcc_categories
212
    original_pcc_orders = list(
213
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)
214
    )
215
    new_pcc_orders = list(
216
        PricingCriteriaCategory.objects.filter(pricing=new_pricing).values_list('order', flat=True)
217
    )
218
    assert new_pcc_orders == original_pcc_orders
219

  
220
    new_pricing = pricing.duplicate(label='Bar')
221
    assert new_pricing.label == 'Bar'
222
    assert new_pricing.slug == 'bar'
223

  
224

  
225
def test_get_agenda_pricing():
226
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
227
    pricing = Pricing.objects.create(label='Foo bar')
228
    event = Event.objects.create(
229
        agenda=agenda, start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)), places=10
230
    )
231

  
232
    # not found
233
    with pytest.raises(AgendaPricingNotFound):
234
        AgendaPricing.get_agenda_pricing(event)
235

  
236
    # ok
237
    agenda_pricing = AgendaPricing.objects.create(
238
        agenda=agenda,
239
        pricing=pricing,
240
        date_start=datetime.date(year=2021, month=9, day=1),
241
        date_end=datetime.date(year=2021, month=10, day=1),
242
    )
243
    assert AgendaPricing.get_agenda_pricing(event) == agenda_pricing
244

  
245
    # more than one matching
246
    AgendaPricing.objects.create(
247
        agenda=agenda,
248
        pricing=pricing,
249
        date_start=datetime.date(year=2021, month=9, day=14),
250
        date_end=datetime.date(year=2021, month=9, day=16),
251
    )
252
    with pytest.raises(AgendaPricingNotFound):
253
        AgendaPricing.get_agenda_pricing(event)
254

  
255

  
256
@pytest.mark.parametrize(
257
    'event_date, found',
258
    [
259
        # just before first day
260
        ((2021, 8, 31, 12, 00), False),
261
        # first day
262
        ((2021, 9, 1, 12, 00), True),
263
        # last day
264
        ((2021, 9, 30, 12, 00), True),
265
        # just after last day
266
        ((2021, 10, 1, 12, 00), False),
267
    ],
268
)
269
def test_get_agenda_pricing_event_date(event_date, found):
270
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
271
    pricing = Pricing.objects.create(label='Foo bar')
272
    event = Event.objects.create(
273
        agenda=agenda, start_datetime=make_aware(datetime.datetime(*event_date)), places=10
274
    )
275
    agenda_pricing = AgendaPricing.objects.create(
276
        agenda=agenda,
277
        pricing=pricing,
278
        date_start=datetime.date(year=2021, month=9, day=1),
279
        date_end=datetime.date(year=2021, month=10, day=1),
280
    )
281
    if found:
282
        assert AgendaPricing.get_agenda_pricing(event) == agenda_pricing
283
    else:
284
        with pytest.raises(AgendaPricingNotFound):
285
            AgendaPricing.get_agenda_pricing(event)
286

  
287

  
288
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
289
def test_get_pricing_context(mock_send, context, nocache):
290
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
291
    event = Event.objects.create(
292
        agenda=agenda, start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)), places=10
293
    )
294
    pricing = Pricing.objects.create(label='Foo bar')
295
    agenda_pricing = AgendaPricing.objects.create(
296
        agenda=agenda,
297
        pricing=pricing,
298
        date_start=datetime.date(year=2021, month=9, day=1),
299
        date_end=datetime.date(year=2021, month=10, day=1),
300
    )
301
    assert agenda_pricing.get_pricing_context(event, 'child:42', 'parent:35') == {}
302
    pricing.extra_variables = {
303
        'foo': 'bar',
304
        'qf': '{{ 40|add:2 }}',
305
        'domicile': 'commune',
306
        'ids': '{{ cards|objects:"foo"|getlist:"id"|join:"," }}',
307
        'bad': '{% if foo %}',
308
    }
309
    pricing.save()
310
    assert agenda_pricing.get_pricing_context(event, 'child:42', 'parent:35') == {
311
        'foo': 'bar',
312
        'qf': '42',
313
        'domicile': 'commune',
314
        'ids': '1,2',
315
    }
316

  
317
    # user_external_id and adult_external_id can be used in variables
318
    pricing.extra_variables = {
319
        'qf': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_id|filter_by:"bar"|filter_value:adult_external_id|list }}',
320
    }
321
    pricing.save()
322
    mock_send.reset_mock()
323
    agenda_pricing.get_pricing_context(event, 'child:42', 'parent:35')
324
    assert 'filter-foo=child%3A42&' in mock_send.call_args_list[0][0][0].url
325
    assert 'filter-bar=parent%3A35&' in mock_send.call_args_list[0][0][0].url
326
    pricing.extra_variables = {
327
        'qf': '{{ cards|objects:"qf"|filter_by:"foo"|filter_value:user_external_raw_id|filter_by:"bar"|filter_value:adult_external_raw_id|list }}',
328
    }
329
    pricing.save()
330
    mock_send.reset_mock()
331
    agenda_pricing.get_pricing_context(event, 'child:42', 'parent:35')
332
    assert 'filter-foo=42&' in mock_send.call_args_list[0][0][0].url
333
    assert 'filter-bar=35&' in mock_send.call_args_list[0][0][0].url
334

  
335

  
336
@pytest.mark.parametrize(
337
    'condition, context, result',
338
    [
339
        ('qf < 1', {}, False),
340
        ('qf < 1', {'qf': 'foo'}, False),
341
        ('qf < 1', {'qf': 1}, False),
342
        ('qf < 1', {'qf': 0.9}, True),
343
        ('1 <= qf and qf < 2', {'qf': 0}, False),
344
        ('1 <= qf and qf < 2', {'qf': 2}, False),
345
        ('1 <= qf and qf < 2', {'qf': 10}, False),
346
        ('1 <= qf and qf < 2', {'qf': 1}, True),
347
        ('1 <= qf and qf < 2', {'qf': 1.5}, True),
348
    ],
349
)
350
def test_compute_condition(condition, context, result):
351
    category = CriteriaCategory.objects.create(label='QF', slug='qf')
352
    criteria = Criteria.objects.create(label='FOO', condition=condition, category=category)
353
    assert criteria.compute_condition(context) == result
354

  
355

  
356
def test_compute_pricing():
357
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
358
    category = CriteriaCategory.objects.create(label='QF', slug='qf')
359
    pricing = Pricing.objects.create(label='Foo bar')
360
    pricing.categories.add(category, through_defaults={'order': 1})
361
    agenda_pricing = AgendaPricing.objects.create(
362
        agenda=agenda,
363
        pricing=pricing,
364
        date_start=datetime.date(year=2021, month=9, day=1),
365
        date_end=datetime.date(year=2021, month=10, day=1),
366
    )
367
    # no criteria defined on agenda_pricing
368
    with pytest.raises(CriteriaConditionNotFound) as e:
369
        agenda_pricing.compute_pricing(context={'qf': 2})
370
    assert e.value.details == {'category': 'qf'}
371

  
372
    # conditions are not set
373
    criteria1 = Criteria.objects.create(label='QF < 1', slug='qf-0', category=category)
374
    criteria2 = Criteria.objects.create(label='QF >= 1', slug='qf-1', category=category)
375
    pricing.criterias.add(criteria1)
376
    pricing.criterias.add(criteria2)
377
    with pytest.raises(CriteriaConditionNotFound) as e:
378
        agenda_pricing.compute_pricing(context={'qf': 2})
379
    assert e.value.details == {'category': 'qf'}
380

  
381
    # conditions set, but no match
382
    criteria1.condition = 'qf < 1'
383
    criteria1.save()
384
    criteria2.condition = 'False'
385
    criteria2.save()
386
    with pytest.raises(CriteriaConditionNotFound) as e:
387
        agenda_pricing.compute_pricing(context={'qf': 2})
388
    assert e.value.details == {'category': 'qf'}
389

  
390
    # criteria found, but agenda_pricing.pricing_data is not defined
391
    criteria1.condition = 'qf < 1'
392
    criteria1.save()
393
    criteria2.condition = 'qf >= 1'
394
    criteria2.save()
395
    with pytest.raises(PricingDataFormatError) as e:
396
        agenda_pricing.compute_pricing(context={'qf': 2})
397
    assert e.value.details == {'category': 'qf', 'pricing': None, 'wanted': 'dict'}
398

  
399
    # criteria not found in pricing_data
400
    agenda_pricing.pricing_data = {
401
        'qf:qf-0': 42,
402
    }
403
    agenda_pricing.save()
404
    with pytest.raises(PricingDataError) as e:
405
        agenda_pricing.compute_pricing(context={'qf': 2})
406
    assert e.value.details == {'category': 'qf', 'criteria': 'qf-1'}
407

  
408
    # criteria found, but value is wrong
409
    for value in ['foo', [], {}]:
410
        agenda_pricing.pricing_data = {
411
            'qf:qf-0': 42,
412
            'qf:qf-1': value,
413
        }
414
        agenda_pricing.save()
415
        with pytest.raises(PricingDataFormatError) as e:
416
            agenda_pricing.compute_pricing(context={'qf': 2})
417
        assert e.value.details == {'pricing': value, 'wanted': 'decimal'}
418

  
419
    # correct value (decimal)
420
    agenda_pricing.pricing_data = {
421
        'qf:qf-0': 42,
422
        'qf:qf-1': 52,
423
    }
424
    agenda_pricing.save()
425
    assert agenda_pricing.compute_pricing(context={'qf': 2}) == (52, {'qf': 'qf-1'})
426

  
427
    # more complexe pricing model
428
    category2 = CriteriaCategory.objects.create(label='Domicile', slug='domicile')
429
    criteria1 = Criteria.objects.create(
430
        label='Commune', slug='dom-0', condition='domicile == "commune"', category=category2
431
    )
432
    criteria2 = Criteria.objects.create(
433
        label='Hors commune', slug='dom-1', condition='domicile != "commune"', category=category2
434
    )
435
    pricing.categories.add(category2, through_defaults={'order': 2})
436
    pricing.criterias.add(criteria1)
437
    pricing.criterias.add(criteria2)
438

  
439
    # wrong definition
440
    agenda_pricing.pricing_data = {
441
        'domicile:dom-0': {
442
            'qf:qf-0': 3,
443
            'qf:qf-1': 5,
444
        },
445
        'domicile:dom-1': {
446
            'qf:qf-0': 7,
447
            'qf:qf-1': 10,
448
        },
449
    }
450
    agenda_pricing.save()
451
    with pytest.raises(PricingDataError) as e:
452
        agenda_pricing.compute_pricing(context={'qf': 2, 'domicile': 'commune'})
453

  
454
    # reorder categories, so the definition is correct
455
    PricingCriteriaCategory.objects.filter(pricing=pricing, category=category).update(order=2)
456
    PricingCriteriaCategory.objects.filter(pricing=pricing, category=category2).update(order=1)
457
    assert agenda_pricing.compute_pricing(context={'qf': 2, 'domicile': 'commune'}) == (
458
        5,
459
        {'domicile': 'dom-0', 'qf': 'qf-1'},
460
    )
461
    assert agenda_pricing.compute_pricing(context={'qf': 0, 'domicile': 'commune'}) == (
462
        3,
463
        {'domicile': 'dom-0', 'qf': 'qf-0'},
464
    )
465
    assert agenda_pricing.compute_pricing(context={'qf': 2, 'domicile': 'ext'}) == (
466
        10,
467
        {'domicile': 'dom-1', 'qf': 'qf-1'},
468
    )
469
    assert agenda_pricing.compute_pricing(context={'qf': 0, 'domicile': 'ext'}) == (
470
        7,
471
        {'domicile': 'dom-1', 'qf': 'qf-0'},
472
    )
473

  
474

  
475
def test_get_booking_modifier_event_not_checked():
476
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
477
    event = Event.objects.create(
478
        agenda=agenda, start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)), places=10
479
    )
480
    pricing = Pricing.objects.create(label='Foo bar')
481
    agenda_pricing = AgendaPricing.objects.create(
482
        agenda=agenda,
483
        pricing=pricing,
484
        date_start=datetime.date(year=2021, month=9, day=1),
485
        date_end=datetime.date(year=2021, month=10, day=1),
486
    )
487
    with pytest.raises(PricingEventNotCheckedError):
488
        agenda_pricing.get_booking_modifier(event, 'child:42')
489

  
490

  
491
def test_get_booking_modifier_no_subscription():
492
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
493
    event = Event.objects.create(
494
        agenda=agenda,
495
        start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
496
        places=10,
497
        checked=True,
498
    )
499
    Subscription.objects.create(
500
        agenda=agenda,
501
        user_external_id='child:35',  # another user
502
        date_start=datetime.date(year=2021, month=9, day=1),
503
        date_end=datetime.date(year=2021, month=10, day=1),
504
    )
505
    pricing = Pricing.objects.create(label='Foo bar')
506
    agenda_pricing = AgendaPricing.objects.create(
507
        agenda=agenda,
508
        pricing=pricing,
509
        date_start=datetime.date(year=2021, month=9, day=1),
510
        date_end=datetime.date(year=2021, month=10, day=1),
511
    )
512
    with pytest.raises(PricingSubscriptionError):
513
        agenda_pricing.get_booking_modifier(event, 'child:42')
514

  
515
    # more than one subscription found !
516
    Subscription.objects.create(
517
        agenda=agenda,
518
        user_external_id='child:42',
519
        date_start=datetime.date(year=2021, month=9, day=1),
520
        date_end=datetime.date(year=2021, month=10, day=1),
521
    )
522
    Subscription.objects.create(
523
        agenda=agenda,
524
        user_external_id='child:42',
525
        date_start=datetime.date(year=2021, month=9, day=1),
526
        date_end=datetime.date(year=2021, month=10, day=1),
527
    )
528
    with pytest.raises(PricingSubscriptionError):
529
        agenda_pricing.get_booking_modifier(event, 'child:42')
530

  
531

  
532
@pytest.mark.parametrize(
533
    'event_date, success',
534
    [
535
        # just before first day
536
        ((2021, 8, 31, 12, 00), False),
537
        # first day
538
        ((2021, 9, 1, 12, 00), True),
539
        # last day
540
        ((2021, 9, 30, 12, 00), True),
541
        # just after last day
542
        ((2021, 10, 1, 12, 00), False),
543
    ],
544
)
545
def test_get_booking_modifier_subscription_date(event_date, success):
546
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
547
    Subscription.objects.create(
548
        agenda=agenda,
549
        user_external_id='child:42',
550
        date_start=datetime.date(year=2021, month=9, day=1),
551
        date_end=datetime.date(year=2021, month=10, day=1),
552
    )
553
    event = Event.objects.create(
554
        agenda=agenda, start_datetime=make_aware(datetime.datetime(*event_date)), places=10, checked=True
555
    )
556
    pricing = Pricing.objects.create(label='Foo bar')
557
    agenda_pricing = AgendaPricing.objects.create(
558
        agenda=agenda,
559
        pricing=pricing,
560
        date_start=datetime.date(year=2021, month=9, day=1),
561
        date_end=datetime.date(year=2021, month=10, day=1),
562
    )
563
    if success:
564
        assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
565
            'status': 'not-booked',
566
            'modifier_type': 'rate',
567
            'modifier_rate': 0,
568
        }
569
    else:
570
        with pytest.raises(PricingSubscriptionError):
571
            agenda_pricing.get_booking_modifier(event, 'child:42')
572

  
573

  
574
def test_get_booking_modifier_no_booking():
575
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
576
    Subscription.objects.create(
577
        agenda=agenda,
578
        user_external_id='child:42',
579
        date_start=datetime.date(year=2021, month=9, day=1),
580
        date_end=datetime.date(year=2021, month=10, day=1),
581
    )
582
    event = Event.objects.create(
583
        agenda=agenda,
584
        start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
585
        places=10,
586
        checked=True,
587
    )
588
    pricing = Pricing.objects.create(label='Foo bar')
589
    agenda_pricing = AgendaPricing.objects.create(
590
        agenda=agenda,
591
        pricing=pricing,
592
        date_start=datetime.date(year=2021, month=9, day=1),
593
        date_end=datetime.date(year=2021, month=10, day=1),
594
    )
595
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
596
        'status': 'not-booked',
597
        'modifier_type': 'rate',
598
        'modifier_rate': 0,
599
    }
600

  
601
    # more than one booking found !
602
    Booking.objects.create(event=event, user_external_id='child:42')
603
    Booking.objects.create(event=event, user_external_id='child:42')
604
    with pytest.raises(PricingMultipleBookingError):
605
        agenda_pricing.get_booking_modifier(event, 'child:42')
606

  
607

  
608
def test_get_booking_modifier_booking_cancelled():
609
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
610
    Subscription.objects.create(
611
        agenda=agenda,
612
        user_external_id='child:42',
613
        date_start=datetime.date(year=2021, month=9, day=1),
614
        date_end=datetime.date(year=2021, month=10, day=1),
615
    )
616
    event = Event.objects.create(
617
        agenda=agenda,
618
        start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
619
        places=10,
620
        checked=True,
621
    )
622
    Booking.objects.create(event=event, user_external_id='child:42', cancellation_datetime=now())
623
    pricing = Pricing.objects.create(label='Foo bar')
624
    agenda_pricing = AgendaPricing.objects.create(
625
        agenda=agenda,
626
        pricing=pricing,
627
        date_start=datetime.date(year=2021, month=9, day=1),
628
        date_end=datetime.date(year=2021, month=10, day=1),
629
    )
630
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
631
        'status': 'cancelled',
632
        'modifier_type': 'rate',
633
        'modifier_rate': 0,
634
    }
635

  
636

  
637
def test_get_booking_modifier_booking_not_checked():
638
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
639
    Subscription.objects.create(
640
        agenda=agenda,
641
        user_external_id='child:42',
642
        date_start=datetime.date(year=2021, month=9, day=1),
643
        date_end=datetime.date(year=2021, month=10, day=1),
644
    )
645
    event = Event.objects.create(
646
        agenda=agenda,
647
        start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
648
        places=10,
649
        checked=True,
650
    )
651
    Booking.objects.create(event=event, user_external_id='child:42')
652
    pricing = Pricing.objects.create(label='Foo bar')
653
    agenda_pricing = AgendaPricing.objects.create(
654
        agenda=agenda,
655
        pricing=pricing,
656
        date_start=datetime.date(year=2021, month=9, day=1),
657
        date_end=datetime.date(year=2021, month=10, day=1),
658
    )
659
    with pytest.raises(PricingBookingNotCheckedError):
660
        agenda_pricing.get_booking_modifier(event, 'child:42')
661

  
662

  
663
def test_get_booking_modifier_booking_absence():
664
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
665
    Subscription.objects.create(
666
        agenda=agenda,
667
        user_external_id='child:42',
668
        date_start=datetime.date(year=2021, month=9, day=1),
669
        date_end=datetime.date(year=2021, month=10, day=1),
670
    )
671
    event = Event.objects.create(
672
        agenda=agenda,
673
        start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
674
        places=10,
675
        checked=True,
676
    )
677
    booking = Booking.objects.create(event=event, user_external_id='child:42', user_was_present=False)
678
    pricing = Pricing.objects.create(label='Foo bar')
679
    agenda_pricing = AgendaPricing.objects.create(
680
        agenda=agenda,
681
        pricing=pricing,
682
        date_start=datetime.date(year=2021, month=9, day=1),
683
        date_end=datetime.date(year=2021, month=10, day=1),
684
    )
685

  
686
    # no check type
687
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
688
        'status': 'absence',
689
        'check_type_group': None,
690
        'check_type': None,
691
        'modifier_type': 'rate',
692
        'modifier_rate': 0,
693
    }
694

  
695
    # check_type but incomplete configuration
696
    group = CheckTypeGroup.objects.create(label='Foo bar')
697
    check_type = CheckType.objects.create(label='Foo reason', group=group, kind='absence')
698
    booking.user_check_type = check_type
699
    booking.save()
700
    with pytest.raises(PricingBookingCheckTypeError) as e:
701
        agenda_pricing.get_booking_modifier(event, 'child:42')
702
    assert e.value.details == {
703
        'check_type_group': 'foo-bar',
704
        'check_type': 'foo-reason',
705
        'reason': 'not-configured',
706
    }
707

  
708
    check_type.pricing = 42
709
    check_type.save()
710
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
711
        'status': 'absence',
712
        'check_type_group': 'foo-bar',
713
        'check_type': 'foo-reason',
714
        'modifier_type': 'fixed',
715
        'modifier_fixed': 42,
716
    }
717

  
718
    check_type.pricing = 0
719
    check_type.save()
720
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
721
        'status': 'absence',
722
        'check_type_group': 'foo-bar',
723
        'check_type': 'foo-reason',
724
        'modifier_type': 'fixed',
725
        'modifier_fixed': 0,
726
    }
727

  
728
    check_type.pricing = None
729
    check_type.pricing_rate = 20
730
    check_type.save()
731
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
732
        'status': 'absence',
733
        'check_type_group': 'foo-bar',
734
        'check_type': 'foo-reason',
735
        'modifier_type': 'rate',
736
        'modifier_rate': 20,
737
    }
738

  
739
    check_type.pricing_rate = 0
740
    check_type.save()
741
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
742
        'status': 'absence',
743
        'check_type_group': 'foo-bar',
744
        'check_type': 'foo-reason',
745
        'modifier_type': 'rate',
746
        'modifier_rate': 0,
747
    }
748

  
749
    # bad check type kind
750
    check_type.kind = 'presence'
751
    check_type.save()
752
    with pytest.raises(PricingBookingCheckTypeError) as e:
753
        agenda_pricing.get_booking_modifier(event, 'child:42')
754
    assert e.value.details == {
755
        'check_type_group': 'foo-bar',
756
        'check_type': 'foo-reason',
757
        'reason': 'wrong-kind',
758
    }
759

  
760

  
761
def test_get_booking_modifier_booking_presence():
762
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
763
    Subscription.objects.create(
764
        agenda=agenda,
765
        user_external_id='child:42',
766
        date_start=datetime.date(year=2021, month=9, day=1),
767
        date_end=datetime.date(year=2021, month=10, day=1),
768
    )
769
    event = Event.objects.create(
770
        agenda=agenda,
771
        start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
772
        places=10,
773
        checked=True,
774
    )
775
    booking = Booking.objects.create(event=event, user_external_id='child:42', user_was_present=True)
776
    pricing = Pricing.objects.create(label='Foo bar')
777
    agenda_pricing = AgendaPricing.objects.create(
778
        agenda=agenda,
779
        pricing=pricing,
780
        date_start=datetime.date(year=2021, month=9, day=1),
781
        date_end=datetime.date(year=2021, month=10, day=1),
782
    )
783

  
784
    # no check type
785
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
786
        'status': 'presence',
787
        'check_type_group': None,
788
        'check_type': None,
789
        'modifier_type': 'rate',
790
        'modifier_rate': 100,
791
    }
792

  
793
    # check_type but incomplete configuration
794
    group = CheckTypeGroup.objects.create(label='Foo bar')
795
    check_type = CheckType.objects.create(label='Foo reason', group=group, kind='presence')
796
    booking.user_check_type = check_type
797
    booking.save()
798
    with pytest.raises(PricingBookingCheckTypeError) as e:
799
        agenda_pricing.get_booking_modifier(event, 'child:42')
800
    assert e.value.details == {
801
        'check_type_group': 'foo-bar',
802
        'check_type': 'foo-reason',
803
        'reason': 'not-configured',
804
    }
805

  
806
    check_type.pricing = 42
807
    check_type.save()
808
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
809
        'status': 'presence',
810
        'check_type_group': 'foo-bar',
811
        'check_type': 'foo-reason',
812
        'modifier_type': 'fixed',
813
        'modifier_fixed': 42,
814
    }
815

  
816
    check_type.pricing = 0
817
    check_type.save()
818
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
819
        'status': 'presence',
820
        'check_type_group': 'foo-bar',
821
        'check_type': 'foo-reason',
822
        'modifier_type': 'fixed',
823
        'modifier_fixed': 0,
824
    }
825

  
826
    check_type.pricing = None
827
    check_type.pricing_rate = 150
828
    check_type.save()
829
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
830
        'status': 'presence',
831
        'check_type_group': 'foo-bar',
832
        'check_type': 'foo-reason',
833
        'modifier_type': 'rate',
834
        'modifier_rate': 150,
835
    }
836

  
837
    check_type.pricing_rate = 0
838
    check_type.save()
839
    assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
840
        'status': 'presence',
841
        'check_type_group': 'foo-bar',
842
        'check_type': 'foo-reason',
843
        'modifier_type': 'rate',
844
        'modifier_rate': 0,
845
    }
846

  
847
    # bad check type kind
848
    check_type.kind = 'absence'
849
    check_type.save()
850
    with pytest.raises(PricingBookingCheckTypeError) as e:
851
        agenda_pricing.get_booking_modifier(event, 'child:42')
852
    assert e.value.details == {
853
        'check_type_group': 'foo-bar',
854
        'check_type': 'foo-reason',
855
        'reason': 'wrong-kind',
856
    }
857

  
858

  
859
def test_get_pricing_data(context):
860
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
861
    event = Event.objects.create(
862
        agenda=agenda,
863
        start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
864
        places=10,
865
        checked=True,
866
    )
867
    Subscription.objects.create(
868
        agenda=agenda,
869
        user_external_id='child:42',
870
        date_start=datetime.date(year=2021, month=9, day=1),
871
        date_end=datetime.date(year=2021, month=10, day=1),
872
    )
873
    category = CriteriaCategory.objects.create(label='Foo', slug='foo')
874
    criteria = Criteria.objects.create(label='Bar', slug='bar', condition='True', category=category)
875
    pricing = Pricing.objects.create(
876
        label='Foo bar',
877
        extra_variables={
878
            'domicile': 'commune',
879
            'qf': '2',
880
        },
881
    )
882
    pricing.criterias.add(criteria)
883
    pricing.categories.add(category, through_defaults={'order': 1})
884
    AgendaPricing.objects.create(
885
        agenda=agenda,
886
        pricing=pricing,
887
        date_start=datetime.date(year=2021, month=9, day=1),
888
        date_end=datetime.date(year=2021, month=10, day=1),
889
        pricing_data={
890
            'foo:bar': 42,
891
        },
892
    )
893
    assert AgendaPricing.get_pricing_data(context['request'], event, 'child:42', 'parent:35') == {
894
        'pricing': 0,
895
        'calculation_details': {
896
            'pricing': 42,
897
            'criterias': {'foo': 'bar'},
898
            'context': {'domicile': 'commune', 'qf': '2'},
899
        },
900
        'booking_details': {
901
            'status': 'not-booked',
902
            'modifier_type': 'rate',
903
            'modifier_rate': 0,
904
        },
905
    }
906

  
907

  
908
@pytest.mark.parametrize(
909
    'modifier, pricing_amount',
910
    [
911
        # not booked
912
        (
913
            {
914
                'status': 'not-booked',
915
                'modifier_type': 'rate',
916
                'modifier_rate': 0,
917
            },
918
            0,
919
        ),
920
        # cancelled
921
        (
922
            {
923
                'status': 'cancelled',
924
                'modifier_type': 'rate',
925
                'modifier_rate': 0,
926
            },
927
            0,
928
        ),
929
        # absence
930
        (
931
            {
932
                'status': 'absence',
933
                'check_type_group': None,
934
                'check_type': None,
935
                'modifier_type': 'rate',
936
                'modifier_rate': 0,
937
            },
938
            0,
939
        ),
940
        (
941
            {
942
                'status': 'absence',
943
                'check_type_group': 'foo-bar',
944
                'check_type': 'foo-reason',
945
                'modifier_type': 'fixed',
946
                'modifier_fixed': 35,
947
            },
948
            35,
949
        ),
950
        (
951
            {
952
                'status': 'absence',
953
                'check_type_group': 'foo-bar',
954
                'check_type': 'foo-reason',
955
                'modifier_type': 'fixed',
956
                'modifier_fixed': 0,
957
            },
958
            0,
959
        ),
960
        (
961
            {
962
                'status': 'absence',
963
                'check_type_group': 'foo-bar',
964
                'check_type': 'foo-reason',
965
                'modifier_type': 'rate',
966
                'modifier_rate': 20,
967
            },
968
            8.4,
969
        ),
970
        (
971
            {
972
                'status': 'absence',
973
                'check_type_group': 'foo-bar',
974
                'check_type': 'foo-reason',
975
                'modifier_type': 'rate',
976
                'modifier_rate': 0,
977
            },
978
            0,
979
        ),
980
        # presence
981
        (
982
            {
983
                'status': 'presence',
984
                'check_type_group': None,
985
                'check_type': None,
986
                'modifier_type': 'rate',
987
                'modifier_rate': 100,
988
            },
989
            42,
990
        ),
991
        (
992
            {
993
                'status': 'presence',
994
                'check_type_group': 'foo-bar',
995
                'check_type': 'foo-reason',
996
                'modifier_type': 'fixed',
997
                'modifier_fixed': 35,
998
            },
999
            35,
1000
        ),
1001
        (
1002
            {
1003
                'status': 'presence',
1004
                'check_type_group': 'foo-bar',
1005
                'check_type': 'foo-reason',
1006
                'modifier_type': 'fixed',
1007
                'modifier_fixed': 0,
1008
            },
1009
            0,
1010
        ),
1011
        (
1012
            {
1013
                'status': 'presence',
1014
                'check_type_group': 'foo-bar',
1015
                'check_type': 'foo-reason',
1016
                'modifier_type': 'rate',
1017
                'modifier_rate': 150,
1018
            },
1019
            63,
1020
        ),
1021
        (
1022
            {
1023
                'status': 'presence',
1024
                'check_type_group': 'foo-bar',
1025
                'check_type': 'foo-reason',
1026
                'modifier_type': 'rate',
1027
                'modifier_rate': 0,
1028
            },
1029
            0,
1030
        ),
1031
    ],
1032
)
1033
def test_aggregate_pricing_data(modifier, pricing_amount):
1034
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1035
    pricing = Pricing.objects.create(label='Foo bar')
1036
    agenda_pricing = AgendaPricing.objects.create(
1037
        agenda=agenda,
1038
        pricing=pricing,
1039
        date_start=datetime.date(year=2021, month=9, day=1),
1040
        date_end=datetime.date(year=2021, month=10, day=1),
1041
    )
1042

  
1043
    assert agenda_pricing.aggregate_pricing_data(
1044
        pricing=42, criterias={'foo': 'bar'}, context={'domicile': 'commune', 'qf': 2}, modifier=modifier
1045
    ) == {
1046
        'pricing': pricing_amount,
1047
        'calculation_details': {
1048
            'pricing': 42,
1049
            'criterias': {'foo': 'bar'},
1050
            'context': {'domicile': 'commune', 'qf': 2},
1051
        },
1052
        'booking_details': modifier,
1053
    }
1054

  
1055

  
1056
def test_agenda_pricing_iter_pricing_matrix_3_categories():
1057
    category1 = CriteriaCategory.objects.create(label='Cat 1')
1058
    criteria11 = Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)
1059
    criteria12 = Criteria.objects.create(label='Crit 1-2', slug='crit-1-2', category=category1, order=2)
1060
    category2 = CriteriaCategory.objects.create(label='Cat 2')
1061
    criteria21 = Criteria.objects.create(label='Crit 2-1', slug='crit-2-1', category=category2, order=1)
1062
    criteria22 = Criteria.objects.create(label='Crit 2-2', slug='crit-2-2', category=category2, order=2)
1063
    criteria23 = Criteria.objects.create(label='Crit 2-3', slug='crit-2-3', category=category2, order=3)
1064
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1065
    criteria31 = Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
1066
    criteria33 = Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
1067
    criteria34 = Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
1068
    criteria32 = Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
1069
    not_used = Criteria.objects.create(label='Not used', slug='crit-3-notused', category=category3, order=5)
1070
    category4 = CriteriaCategory.objects.create(label='Cat 4')  # ignored
1071

  
1072
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1073
    pricing = Pricing.objects.create(label='Foo bar')
1074
    pricing.categories.add(category1, through_defaults={'order': 1})
1075
    pricing.categories.add(category2, through_defaults={'order': 2})
1076
    pricing.categories.add(category3, through_defaults={'order': 3})
1077
    pricing.categories.add(category4, through_defaults={'order': 4})
1078
    pricing.criterias.set(Criteria.objects.exclude(pk=not_used.pk))
1079
    agenda_pricing = AgendaPricing.objects.create(
1080
        agenda=agenda,
1081
        pricing=pricing,
1082
        date_start=datetime.date(year=2021, month=9, day=1),
1083
        date_end=datetime.date(year=2021, month=10, day=1),
1084
    )
1085
    assert list(agenda_pricing.iter_pricing_matrix()) == [
1086
        PricingMatrix(
1087
            criteria=criteria11,
1088
            rows=[
1089
                PricingMatrixRow(
1090
                    criteria=criteria31,
1091
                    cells=[
1092
                        PricingMatrixCell(criteria=criteria21, value=None),
1093
                        PricingMatrixCell(criteria=criteria22, value=None),
1094
                        PricingMatrixCell(criteria=criteria23, value=None),
1095
                    ],
1096
                ),
1097
                PricingMatrixRow(
1098
                    criteria=criteria32,
1099
                    cells=[
1100
                        PricingMatrixCell(criteria=criteria21, value=None),
1101
                        PricingMatrixCell(criteria=criteria22, value=None),
1102
                        PricingMatrixCell(criteria=criteria23, value=None),
1103
                    ],
1104
                ),
1105
                PricingMatrixRow(
1106
                    criteria=criteria33,
1107
                    cells=[
1108
                        PricingMatrixCell(criteria=criteria21, value=None),
1109
                        PricingMatrixCell(criteria=criteria22, value=None),
1110
                        PricingMatrixCell(criteria=criteria23, value=None),
1111
                    ],
1112
                ),
1113
                PricingMatrixRow(
1114
                    criteria=criteria34,
1115
                    cells=[
1116
                        PricingMatrixCell(criteria=criteria21, value=None),
1117
                        PricingMatrixCell(criteria=criteria22, value=None),
1118
                        PricingMatrixCell(criteria=criteria23, value=None),
1119
                    ],
1120
                ),
1121
            ],
1122
        ),
1123
        PricingMatrix(
1124
            criteria=criteria12,
1125
            rows=[
1126
                PricingMatrixRow(
1127
                    criteria=criteria31,
1128
                    cells=[
1129
                        PricingMatrixCell(criteria=criteria21, value=None),
1130
                        PricingMatrixCell(criteria=criteria22, value=None),
1131
                        PricingMatrixCell(criteria=criteria23, value=None),
1132
                    ],
1133
                ),
1134
                PricingMatrixRow(
1135
                    criteria=criteria32,
1136
                    cells=[
1137
                        PricingMatrixCell(criteria=criteria21, value=None),
1138
                        PricingMatrixCell(criteria=criteria22, value=None),
1139
                        PricingMatrixCell(criteria=criteria23, value=None),
1140
                    ],
1141
                ),
1142
                PricingMatrixRow(
1143
                    criteria=criteria33,
1144
                    cells=[
1145
                        PricingMatrixCell(criteria=criteria21, value=None),
1146
                        PricingMatrixCell(criteria=criteria22, value=None),
1147
                        PricingMatrixCell(criteria=criteria23, value=None),
1148
                    ],
1149
                ),
1150
                PricingMatrixRow(
1151
                    criteria=criteria34,
1152
                    cells=[
1153
                        PricingMatrixCell(criteria=criteria21, value=None),
1154
                        PricingMatrixCell(criteria=criteria22, value=None),
1155
                        PricingMatrixCell(criteria=criteria23, value=None),
1156
                    ],
1157
                ),
1158
            ],
1159
        ),
1160
    ]
1161

  
1162
    # some data defined
1163
    agenda_pricing.pricing_data = {
1164
        'cat-1:crit-1-1': {
1165
            'cat-2:crit-2-1': {
1166
                'cat-3:crit-3-1': 111,
1167
                'cat-3:crit-3-3': 'not-a-decimal',
1168
                'cat-3:crit-3-4': 114,
1169
            },
1170
            'cat-2:crit-2-3': {
1171
                'cat-3:crit-3-2': 132,
1172
            },
1173
        },
1174
        'cat-1:crit-1-2': {
1175
            'cat-2:crit-2-2': {
1176
                'cat-3:crit-3-3': 223,
1177
            },
1178
        },
1179
    }
1180
    agenda_pricing.save()
1181
    assert list(agenda_pricing.iter_pricing_matrix()) == [
1182
        PricingMatrix(
1183
            criteria=criteria11,
1184
            rows=[
1185
                PricingMatrixRow(
1186
                    criteria=criteria31,
1187
                    cells=[
1188
                        PricingMatrixCell(criteria=criteria21, value=111),
1189
                        PricingMatrixCell(criteria=criteria22, value=None),
1190
                        PricingMatrixCell(criteria=criteria23, value=None),
1191
                    ],
1192
                ),
1193
                PricingMatrixRow(
1194
                    criteria=criteria32,
1195
                    cells=[
1196
                        PricingMatrixCell(criteria=criteria21, value=None),
1197
                        PricingMatrixCell(criteria=criteria22, value=None),
1198
                        PricingMatrixCell(criteria=criteria23, value=132),
1199
                    ],
1200
                ),
1201
                PricingMatrixRow(
1202
                    criteria=criteria33,
1203
                    cells=[
1204
                        PricingMatrixCell(criteria=criteria21, value=None),
1205
                        PricingMatrixCell(criteria=criteria22, value=None),
1206
                        PricingMatrixCell(criteria=criteria23, value=None),
1207
                    ],
1208
                ),
1209
                PricingMatrixRow(
1210
                    criteria=criteria34,
1211
                    cells=[
1212
                        PricingMatrixCell(criteria=criteria21, value=114),
1213
                        PricingMatrixCell(criteria=criteria22, value=None),
1214
                        PricingMatrixCell(criteria=criteria23, value=None),
1215
                    ],
1216
                ),
1217
            ],
1218
        ),
1219
        PricingMatrix(
1220
            criteria=criteria12,
1221
            rows=[
1222
                PricingMatrixRow(
1223
                    criteria=criteria31,
1224
                    cells=[
1225
                        PricingMatrixCell(criteria=criteria21, value=None),
1226
                        PricingMatrixCell(criteria=criteria22, value=None),
1227
                        PricingMatrixCell(criteria=criteria23, value=None),
1228
                    ],
1229
                ),
1230
                PricingMatrixRow(
1231
                    criteria=criteria32,
1232
                    cells=[
1233
                        PricingMatrixCell(criteria=criteria21, value=None),
1234
                        PricingMatrixCell(criteria=criteria22, value=None),
1235
                        PricingMatrixCell(criteria=criteria23, value=None),
1236
                    ],
1237
                ),
1238
                PricingMatrixRow(
1239
                    criteria=criteria33,
1240
                    cells=[
1241
                        PricingMatrixCell(criteria=criteria21, value=None),
1242
                        PricingMatrixCell(criteria=criteria22, value=223),
1243
                        PricingMatrixCell(criteria=criteria23, value=None),
1244
                    ],
1245
                ),
1246
                PricingMatrixRow(
1247
                    criteria=criteria34,
1248
                    cells=[
1249
                        PricingMatrixCell(criteria=criteria21, value=None),
1250
                        PricingMatrixCell(criteria=criteria22, value=None),
1251
                        PricingMatrixCell(criteria=criteria23, value=None),
1252
                    ],
1253
                ),
1254
            ],
1255
        ),
1256
    ]
1257

  
1258

  
1259
def test_agenda_pricing_iter_pricing_matrix_2_categories():
1260
    category2 = CriteriaCategory.objects.create(label='Cat 2')
1261
    criteria21 = Criteria.objects.create(label='Crit 2-1', slug='crit-2-1', category=category2, order=1)
1262
    criteria22 = Criteria.objects.create(label='Crit 2-2', slug='crit-2-2', category=category2, order=2)
1263
    criteria23 = Criteria.objects.create(label='Crit 2-3', slug='crit-2-3', category=category2, order=3)
1264
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1265
    criteria31 = Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
1266
    criteria33 = Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
1267
    criteria34 = Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
1268
    criteria32 = Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
1269
    not_used = Criteria.objects.create(label='Not used', slug='crit-3-notused', category=category3, order=5)
1270

  
1271
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1272
    pricing = Pricing.objects.create(label='Foo bar')
1273
    pricing.categories.add(category2, through_defaults={'order': 2})
1274
    pricing.categories.add(category3, through_defaults={'order': 3})
1275
    pricing.criterias.set(Criteria.objects.exclude(pk=not_used.pk))
1276
    agenda_pricing = AgendaPricing.objects.create(
1277
        agenda=agenda,
1278
        pricing=pricing,
1279
        date_start=datetime.date(year=2021, month=9, day=1),
1280
        date_end=datetime.date(year=2021, month=10, day=1),
1281
    )
1282

  
1283
    assert list(agenda_pricing.iter_pricing_matrix()) == [
1284
        PricingMatrix(
1285
            criteria=None,
1286
            rows=[
1287
                PricingMatrixRow(
1288
                    criteria=criteria31,
1289
                    cells=[
1290
                        PricingMatrixCell(criteria=criteria21, value=None),
1291
                        PricingMatrixCell(criteria=criteria22, value=None),
1292
                        PricingMatrixCell(criteria=criteria23, value=None),
1293
                    ],
1294
                ),
1295
                PricingMatrixRow(
1296
                    criteria=criteria32,
1297
                    cells=[
1298
                        PricingMatrixCell(criteria=criteria21, value=None),
1299
                        PricingMatrixCell(criteria=criteria22, value=None),
1300
                        PricingMatrixCell(criteria=criteria23, value=None),
1301
                    ],
1302
                ),
1303
                PricingMatrixRow(
1304
                    criteria=criteria33,
1305
                    cells=[
1306
                        PricingMatrixCell(criteria=criteria21, value=None),
1307
                        PricingMatrixCell(criteria=criteria22, value=None),
1308
                        PricingMatrixCell(criteria=criteria23, value=None),
1309
                    ],
1310
                ),
1311
                PricingMatrixRow(
1312
                    criteria=criteria34,
1313
                    cells=[
1314
                        PricingMatrixCell(criteria=criteria21, value=None),
1315
                        PricingMatrixCell(criteria=criteria22, value=None),
1316
                        PricingMatrixCell(criteria=criteria23, value=None),
1317
                    ],
1318
                ),
1319
            ],
1320
        ),
1321
    ]
1322

  
1323
    # some data defined
1324
    agenda_pricing.pricing_data = {
1325
        'cat-2:crit-2-1': {
1326
            'cat-3:crit-3-1': 11,
1327
            'cat-3:crit-3-3': 'not-a-decimal',
1328
            'cat-3:crit-3-4': 14,
1329
        },
1330
        'cat-2:crit-2-3': {
1331
            'cat-3:crit-3-2': 32,
1332
        },
1333
    }
1334
    agenda_pricing.save()
1335
    assert list(agenda_pricing.iter_pricing_matrix()) == [
1336
        PricingMatrix(
1337
            criteria=None,
1338
            rows=[
1339
                PricingMatrixRow(
1340
                    criteria=criteria31,
1341
                    cells=[
1342
                        PricingMatrixCell(criteria=criteria21, value=11),
1343
                        PricingMatrixCell(criteria=criteria22, value=None),
1344
                        PricingMatrixCell(criteria=criteria23, value=None),
1345
                    ],
1346
                ),
1347
                PricingMatrixRow(
1348
                    criteria=criteria32,
1349
                    cells=[
1350
                        PricingMatrixCell(criteria=criteria21, value=None),
1351
                        PricingMatrixCell(criteria=criteria22, value=None),
1352
                        PricingMatrixCell(criteria=criteria23, value=32),
1353
                    ],
1354
                ),
1355
                PricingMatrixRow(
1356
                    criteria=criteria33,
1357
                    cells=[
1358
                        PricingMatrixCell(criteria=criteria21, value=None),
1359
                        PricingMatrixCell(criteria=criteria22, value=None),
1360
                        PricingMatrixCell(criteria=criteria23, value=None),
1361
                    ],
1362
                ),
1363
                PricingMatrixRow(
1364
                    criteria=criteria34,
1365
                    cells=[
1366
                        PricingMatrixCell(criteria=criteria21, value=14),
1367
                        PricingMatrixCell(criteria=criteria22, value=None),
1368
                        PricingMatrixCell(criteria=criteria23, value=None),
1369
                    ],
1370
                ),
1371
            ],
1372
        ),
1373
    ]
1374

  
1375

  
1376
def test_agenda_pricing_iter_pricing_matrix_1_category():
1377
    category3 = CriteriaCategory.objects.create(label='Cat 3')
1378
    criteria31 = Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
1379
    criteria33 = Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
1380
    criteria34 = Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
1381
    criteria32 = Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
1382
    not_used = Criteria.objects.create(label='Not used', slug='crit-3-notused', category=category3, order=5)
1383

  
1384
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1385
    pricing = Pricing.objects.create(label='Foo bar')
1386
    pricing.categories.add(category3, through_defaults={'order': 3})
1387
    pricing.criterias.set(Criteria.objects.exclude(pk=not_used.pk))
1388
    agenda_pricing = AgendaPricing.objects.create(
1389
        agenda=agenda,
1390
        pricing=pricing,
1391
        date_start=datetime.date(year=2021, month=9, day=1),
1392
        date_end=datetime.date(year=2021, month=10, day=1),
1393
    )
1394

  
1395
    assert list(agenda_pricing.iter_pricing_matrix()) == [
1396
        PricingMatrix(
1397
            criteria=None,
1398
            rows=[
1399
                PricingMatrixRow(
1400
                    criteria=criteria31,
1401
                    cells=[
1402
                        PricingMatrixCell(criteria=None, value=None),
1403
                    ],
1404
                ),
1405
                PricingMatrixRow(
1406
                    criteria=criteria32,
1407
                    cells=[
1408
                        PricingMatrixCell(criteria=None, value=None),
1409
                    ],
1410
                ),
1411
                PricingMatrixRow(
1412
                    criteria=criteria33,
1413
                    cells=[
1414
                        PricingMatrixCell(criteria=None, value=None),
1415
                    ],
1416
                ),
1417
                PricingMatrixRow(
1418
                    criteria=criteria34,
1419
                    cells=[
1420
                        PricingMatrixCell(criteria=None, value=None),
1421
                    ],
1422
                ),
1423
            ],
1424
        ),
1425
    ]
1426

  
1427
    # some data defined
1428
    agenda_pricing.pricing_data = {
1429
        'cat-3:crit-3-1': 1,
1430
        'cat-3:crit-3-3': 'not-a-decimal',
1431
        'cat-3:crit-3-4': 4,
1432
    }
1433
    agenda_pricing.save()
1434
    assert list(agenda_pricing.iter_pricing_matrix()) == [
1435
        PricingMatrix(
1436
            criteria=None,
1437
            rows=[
1438
                PricingMatrixRow(
1439
                    criteria=criteria31,
1440
                    cells=[
1441
                        PricingMatrixCell(criteria=None, value=1),
1442
                    ],
1443
                ),
1444
                PricingMatrixRow(
1445
                    criteria=criteria32,
1446
                    cells=[
1447
                        PricingMatrixCell(criteria=None, value=None),
1448
                    ],
1449
                ),
1450
                PricingMatrixRow(
1451
                    criteria=criteria33,
1452
                    cells=[
1453
                        PricingMatrixCell(criteria=None, value=None),
1454
                    ],
1455
                ),
1456
                PricingMatrixRow(
1457
                    criteria=criteria34,
1458
                    cells=[
1459
                        PricingMatrixCell(criteria=None, value=4),
1460
                    ],
1461
                ),
1462
            ],
1463
        ),
1464
    ]
1465

  
1466

  
1467
def test_agenda_pricing_iter_pricing_matrix_empty():
1468
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
1469
    pricing = Pricing.objects.create(label='Foo bar')
1470
    agenda_pricing = AgendaPricing.objects.create(
1471
        agenda=agenda,
1472
        pricing=pricing,
1473
        date_start=datetime.date(year=2021, month=9, day=1),
1474
        date_end=datetime.date(year=2021, month=10, day=1),
1475
    )
1476

  
1477
    assert list(agenda_pricing.iter_pricing_matrix()) == []
tests/settings.py
34 34
EXCEPTIONS_SOURCES = {}
35 35

  
36 36
SITE_BASE_URL = 'https://example.com'
37
CHRONO_ENABLE_PRICING = True
tests/test_import_export.py
35 35
    VirtualMember,
36 36
)
37 37
from chrono.manager.utils import import_site
38
from chrono.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
39 38

  
40 39
pytestmark = pytest.mark.django_db
41 40

  
......
486 485
    assert agenda.events_type == events_type
487 486

  
488 487

  
489
def test_import_export_agenda_with_pricing(app):
490
    pricing = Pricing.objects.create(label='Foo')
491
    agenda = Agenda.objects.create(label='Foo Bar', kind='events')
492
    Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
493
    agenda_pricing = AgendaPricing.objects.create(
494
        agenda=agenda,
495
        pricing=pricing,
496
        date_start=datetime.date(year=2021, month=9, day=1),
497
        date_end=datetime.date(year=2021, month=10, day=1),
498
        pricing_data={
499
            'foo': 'bar',
500
        },
501
    )
502
    output = get_output_of_command('export_site')
503

  
504
    import_site(data={}, clean=True)
505
    assert Agenda.objects.count() == 0
506
    assert Pricing.objects.count() == 0
507
    data = json.loads(output)
508
    del data['pricing_models']
509
    print(data)
510

  
511
    with pytest.raises(AgendaImportError) as excinfo:
512
        import_site(data, overwrite=True)
513
    assert str(excinfo.value) == 'Missing "foo" pricing model'
514

  
515
    Pricing.objects.create(label='foobar')
516
    with pytest.raises(AgendaImportError) as excinfo:
517
        import_site(data, overwrite=True)
518
    assert str(excinfo.value) == 'Missing "foo" pricing model'
519

  
520
    pricing = Pricing.objects.create(label='Foo')
521
    import_site(data, overwrite=True)
522
    agenda = Agenda.objects.get(slug=agenda.slug)
523
    assert agenda.agendapricing_set.count() == 1
524
    agenda_pricing = agenda.agendapricing_set.get()
525
    assert agenda_pricing.agenda == agenda
526
    assert agenda_pricing.pricing == pricing
527
    assert agenda_pricing.date_start == datetime.date(year=2021, month=9, day=1)
528
    assert agenda_pricing.date_end == datetime.date(year=2021, month=10, day=1)
529

  
530
    # again
531
    import_site(data)
532
    assert agenda.agendapricing_set.count() == 1
533
    agenda_pricing = AgendaPricing.objects.get(pk=agenda_pricing.pk)
534
    assert agenda_pricing.agenda == agenda
535
    assert agenda_pricing.pricing == pricing
536

  
537
    data['agendas'][0]['pricings'].append(
538
        {
539
            'pricing': 'foo',
540
            'date_start': '2022-09-01',
541
            'date_end': '2022-10-01',
542
            'pricing_data': {'foo': 'bar'},
543
        }
544
    )
545
    import_site(data)
546
    assert agenda.agendapricing_set.count() == 2
547
    agenda_pricing = AgendaPricing.objects.latest('pk')
548
    assert agenda_pricing.agenda == agenda
549
    assert agenda_pricing.pricing == pricing
550
    assert agenda_pricing.date_start == datetime.date(year=2022, month=9, day=1)
551
    assert agenda_pricing.date_end == datetime.date(year=2022, month=10, day=1)
552

  
553

  
554 488
def test_import_export_virtual_agenda(app):
555 489
    virtual_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
556 490
    output = get_output_of_command('export_site')
......
1166 1100
    assert resource.slug == 'foo-bar'
1167 1101

  
1168 1102

  
1169
def test_import_export_pricing_criteria_category(app):
1170
    output = get_output_of_command('export_site')
1171
    payload = json.loads(output)
1172
    assert len(payload['pricing_categories']) == 0
1173

  
1174
    category = CriteriaCategory.objects.create(label='Foo bar')
1175
    Criteria.objects.create(label='Foo reason', category=category)
1176
    Criteria.objects.create(label='Baz', category=category)
1177

  
1178
    output = get_output_of_command('export_site')
1179
    payload = json.loads(output)
1180
    assert len(payload['pricing_categories']) == 1
1181

  
1182
    category.delete()
1183
    assert not CriteriaCategory.objects.exists()
1184
    assert not Criteria.objects.exists()
1185

  
1186
    import_site(copy.deepcopy(payload))
1187
    assert CriteriaCategory.objects.count() == 1
1188
    category = CriteriaCategory.objects.first()
1189
    assert category.label == 'Foo bar'
1190
    assert category.slug == 'foo-bar'
1191
    assert category.criterias.count() == 2
1192
    assert Criteria.objects.get(category=category, label='Foo reason', slug='foo-reason')
1193
    assert Criteria.objects.get(category=category, label='Baz', slug='baz')
1194

  
1195
    # update
1196
    update_payload = copy.deepcopy(payload)
1197
    update_payload['pricing_categories'][0]['label'] = 'Foo bar Updated'
1198
    import_site(update_payload)
1199
    category.refresh_from_db()
1200
    assert category.label == 'Foo bar Updated'
1201

  
1202
    # insert another category
1203
    category.slug = 'foo-bar-updated'
1204
    category.save()
1205
    import_site(copy.deepcopy(payload))
1206
    assert CriteriaCategory.objects.count() == 2
1207
    category = CriteriaCategory.objects.latest('pk')
1208
    assert category.label == 'Foo bar'
1209
    assert category.slug == 'foo-bar'
1210
    assert category.criterias.count() == 2
1211
    assert Criteria.objects.get(category=category, label='Foo reason', slug='foo-reason')
1212
    assert Criteria.objects.get(category=category, label='Baz', slug='baz')
1213

  
1214
    # with overwrite
1215
    Criteria.objects.create(category=category, label='Baz2')
1216
    import_site(copy.deepcopy(payload), overwrite=True)
1217
    assert CriteriaCategory.objects.count() == 2
1218
    category = CriteriaCategory.objects.latest('pk')
1219
    assert category.label == 'Foo bar'
1220
    assert category.slug == 'foo-bar'
1221
    assert category.criterias.count() == 2
1222
    assert Criteria.objects.get(category=category, label='Foo reason', slug='foo-reason')
1223
    assert Criteria.objects.get(category=category, label='Baz', slug='baz')
1224

  
1225

  
1226
def test_import_export_pricing(app):
1227
    output = get_output_of_command('export_site')
1228
    payload = json.loads(output)
1229
    assert len(payload['pricing_models']) == 0
1230

  
1231
    pricing = Pricing.objects.create(label='Foo bar', extra_variables={'foo': 'bar'})
1232

  
1233
    output = get_output_of_command('export_site')
1234
    payload = json.loads(output)
1235
    assert len(payload['pricing_models']) == 1
1236

  
1237
    pricing.delete()
1238
    assert not Pricing.objects.exists()
1239

  
1240
    import_site(copy.deepcopy(payload))
1241
    assert Pricing.objects.count() == 1
1242
    pricing = Pricing.objects.first()
1243
    assert pricing.label == 'Foo bar'
1244
    assert pricing.slug == 'foo-bar'
1245
    assert pricing.extra_variables == {'foo': 'bar'}
1246

  
1247
    # update
1248
    update_payload = copy.deepcopy(payload)
1249
    update_payload['pricing_models'][0]['label'] = 'Foo bar Updated'
1250
    import_site(update_payload)
1251
    pricing.refresh_from_db()
1252
    assert pricing.label == 'Foo bar Updated'
1253

  
1254
    # insert another pricing
1255
    pricing.slug = 'foo-bar-updated'
1256
    pricing.save()
1257
    import_site(copy.deepcopy(payload))
1258
    assert Pricing.objects.count() == 2
1259
    pricing = Pricing.objects.latest('pk')
1260
    assert pricing.label == 'Foo bar'
1261
    assert pricing.slug == 'foo-bar'
1262
    assert pricing.extra_variables == {'foo': 'bar'}
1263

  
1264

  
1265
def test_import_export_pricing_with_categories(app):
1266
    pricing = Pricing.objects.create(label='Foo bar')
1267
    category = CriteriaCategory.objects.create(label='Foo bar')
1268
    pricing.categories.add(category, through_defaults={'order': 42})
1269

  
1270
    output = get_output_of_command('export_site')
1271

  
1272
    import_site(data={}, clean=True)
1273
    assert Pricing.objects.count() == 0
1274
    assert CriteriaCategory.objects.count() == 0
1275
    data = json.loads(output)
1276
    del data['pricing_categories']
1277

  
1278
    with pytest.raises(AgendaImportError) as excinfo:
1279
        import_site(data, overwrite=True)
1280
    assert str(excinfo.value) == 'Missing "foo-bar" pricing category'
1281

  
1282
    CriteriaCategory.objects.create(label='Foobar')
1283
    with pytest.raises(AgendaImportError) as excinfo:
1284
        import_site(data, overwrite=True)
1285
    assert str(excinfo.value) == 'Missing "foo-bar" pricing category'
1286

  
1287
    category = CriteriaCategory.objects.create(label='Foo bar')
1288
    import_site(data, overwrite=True)
1289
    pricing = Pricing.objects.get(slug=pricing.slug)
1290
    assert list(pricing.categories.all()) == [category]
1291
    assert PricingCriteriaCategory.objects.first().order == 42
1292

  
1293
    category2 = CriteriaCategory.objects.create(label='Foo bar 2')
1294
    category3 = CriteriaCategory.objects.create(label='Foo bar 3')
1295
    pricing.categories.add(category2, through_defaults={'order': 1})
1296
    output = get_output_of_command('export_site')
1297
    data = json.loads(output)
1298
    del data['pricing_categories']
1299
    data['pricing_models'][0]['categories'] = [
1300
        {
1301
            'category': 'foo-bar-3',
1302
            'order': 1,
1303
            'criterias': [],
1304
        },
1305
        {
1306
            'category': 'foo-bar',
1307
            'order': 35,
1308
            'criterias': [],
1309
        },
1310
    ]
1311
    import_site(data, overwrite=True)
1312
    assert list(pricing.categories.all()) == [category, category3]
1313
    assert list(
1314
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
1315
    ) == [category3.pk, category.pk]
1316
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
1317
        1,
1318
        35,
1319
    ]
1320
    assert list(pricing.criterias.all()) == []
1321

  
1322
    criteria1 = Criteria.objects.create(label='Crit 1', category=category)
1323
    Criteria.objects.create(label='Crit 2', category=category)
1324
    criteria3 = Criteria.objects.create(label='Crit 3', category=category)
1325

  
1326
    # unknown criteria
1327
    data['pricing_models'][0]['categories'] = [
1328
        {
1329
            'category': 'foo-bar-3',
1330
            'order': 1,
1331
            'criterias': ['unknown'],
1332
        },
1333
        {
1334
            'category': 'foo-bar',
1335
            'order': 35,
1336
            'criterias': [],
1337
        },
1338
    ]
1339
    with pytest.raises(AgendaImportError) as excinfo:
1340
        import_site(data, overwrite=True)
1341
    assert str(excinfo.value) == 'Missing "unknown" pricing criteria for "foo-bar-3" category'
1342

  
1343
    # wrong criteria (from another category)
1344
    data['pricing_models'][0]['categories'] = [
1345
        {
1346
            'category': 'foo-bar-3',
1347
            'order': 1,
1348
            'criterias': ['crit-1'],
1349
        },
1350
        {
1351
            'category': 'foo-bar',
1352
            'order': 35,
1353
            'criterias': [],
1354
        },
1355
    ]
1356
    with pytest.raises(AgendaImportError) as excinfo:
1357
        import_site(data, overwrite=True)
1358
    assert str(excinfo.value) == 'Missing "crit-1" pricing criteria for "foo-bar-3" category'
1359

  
1360
    data['pricing_models'][0]['categories'] = [
1361
        {
1362
            'category': 'foo-bar-3',
1363
            'order': 1,
1364
            'criterias': [],
1365
        },
1366
        {
1367
            'category': 'foo-bar',
1368
            'order': 35,
1369
            'criterias': ['crit-1', 'crit-3'],
1370
        },
1371
    ]
1372
    import_site(data, overwrite=True)
1373
    assert list(pricing.categories.all()) == [category, category3]
1374
    assert list(
1375
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
1376
    ) == [category3.pk, category.pk]
1377
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
1378
        1,
1379
        35,
1380
    ]
1381
    assert set(pricing.criterias.all()) == {criteria1, criteria3}
1382

  
1383

  
1384 1103
@mock.patch('chrono.agendas.models.Agenda.is_available_for_simple_management')
1385 1104
def test_import_export_desk_simple_management(available_mock):
1386 1105
    agenda = Agenda.objects.create(label='Foo bar', kind='meetings', desk_simple_management=True)
1387
-