pricing: pricing configuration on events agenda (#65053)

This commit is contained in:
Lauréline Guérin 2022-05-10 17:03:43 +02:00
parent 11f5fa3506
commit 7f85145858
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
10 changed files with 476 additions and 4 deletions

View File

@ -6,7 +6,7 @@
{% if desk %}
<a href="{% url 'chrono-manager-agenda-settings' desk.agenda_id %}">{% trans "Settings" %}</a>
{% else %}
<a href=".">{% trans "Settings" %}</a>
<a href="{% url 'chrono-manager-agenda-settings' agenda.id|default:object.id %}">{% trans "Settings" %}</a>
{% endif %}
{% endblock %}

View File

@ -4,6 +4,9 @@
{% block agenda-extra-management-actions %}
<a rel="popup" href="{% url 'chrono-manager-agenda-import-events' pk=object.id %}">{% trans 'Import Events' %}</a>
<a rel="popup" href="{% url 'chrono-manager-agenda-add-event' pk=object.id %}">{% trans 'New Event' %}</a>
{% if pricing_enabled %}
<a rel="popup" href="{% url 'chrono-manager-agenda-pricing-add' pk=object.id %}">{% trans 'New pricing' %}</a>
{% endif %}
{% endblock %}
{% block agenda-settings %}
@ -90,6 +93,28 @@
</div>
</div>
{% if pricing_enabled %}
<div class="section">
<h3>{% trans "Pricing" context 'pricing' %}</h3>
<div>
{% if agenda_pricings %}
<ul class="objects-list single-links">
{% for agenda_pricing in agenda_pricings %}
<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>
{% endfor %}
</ul>
{% else %}
<div class="big-msg-info">
{% blocktrans %}
This agenda doesn't have any pricing definied yet. Click on the "New pricing" button in
the top right of the page to add a first one.
{% endblocktrans %}
</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="section">
<h3>{% trans "Notifications" %}
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-notifications-settings' pk=object.id %}">{% trans 'Configure' %}</a>

View File

@ -87,6 +87,7 @@ from chrono.agendas.models import (
UnavailabilityCalendar,
VirtualMember,
)
from chrono.pricing.models import AgendaPricing
from chrono.utils.date import get_weekday_index
from .forms import (
@ -1903,6 +1904,12 @@ class AgendaSettings(ManagedAgendaMixin, DetailView):
end_datetime__gt=now(),
)
context['desk'] = desk
context['pricing_enabled'] = settings.CHRONO_ENABLE_PRICING
context['agenda_pricings'] = (
AgendaPricing.objects.filter(agenda=self.agenda)
.select_related('pricing')
.order_by('date_start', 'date_end')
)
return context
def get_events(self):

View File

@ -19,7 +19,7 @@ from django.forms import ValidationError
from django.template import Template, TemplateSyntaxError
from django.utils.translation import ugettext_lazy as _
from chrono.pricing.models import Criteria, CriteriaCategory
from chrono.pricing.models import AgendaPricing, Criteria, CriteriaCategory
class NewCriteriaForm(forms.ModelForm):
@ -86,3 +86,31 @@ class PricingCriteriaCategoryEditForm(forms.Form):
super().__init__(*args, **kwargs)
self.fields['criterias'].queryset = self.category.criterias.all()
self.initial['criterias'] = self.pricing.criterias.filter(category=self.category)
class AgendaPricingForm(forms.ModelForm):
class Meta:
model = AgendaPricing
fields = ['pricing', 'date_start', 'date_end']
widgets = {
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
}
def clean(self):
cleaned_data = super().clean()
if 'date_start' in cleaned_data and 'date_end' in cleaned_data:
if cleaned_data['date_end'] <= cleaned_data['date_start']:
self.add_error('date_end', _('End date must be greater than start date.'))
else:
overlapping_qs = AgendaPricing.objects.filter(agenda=self.instance.agenda,).extra(
where=["(date_start, date_end) OVERLAPS (%s, %s)"],
params=[cleaned_data['date_start'], cleaned_data['date_end']],
)
if self.instance.pk:
overlapping_qs = overlapping_qs.exclude(pk=self.instance.pk)
if overlapping_qs.exists():
raise forms.ValidationError(_('Pricing overlaps existing pricings.'))
return cleaned_data

View File

@ -0,0 +1,25 @@
{% extends "chrono/manager_agenda_settings.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-agenda-pricing-detail' agenda.pk object.pk %}">{{ object.pricing }}</a>
{% endblock %}
{% block appbar %}
<h2>
{{ object.pricing }} ({{ object.date_start|date:'d/m/Y' }} - {{ object.date_end|date:'d/m/Y' }})
</h2>
{% if user_can_manage %}
<span class="actions">
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
<li><a rel="popup" href="{% url 'chrono-manager-agenda-pricing-edit' agenda.pk object.pk %}">{% trans 'Options' %}</a></li>
<li><a rel="popup" href="{% url 'chrono-manager-agenda-pricing-delete' agenda.pk object.pk %}">{% trans 'Delete' %}</a></li>
</ul>
</span>
{% endif %}
{% endblock %}
{% block content %}
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends "chrono/manager_agenda_settings.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
{% if form.instance.pk %}
<a href="{% url 'chrono-manager-agenda-pricing-detail' agenda.pk object.pk %}">{{ object.pricing }}</a>
<a href="{% url 'chrono-manager-agenda-pricing-edit' agenda.pk object.pk %}">{% trans "Edit" %}</a>
{% else %}
<a href="{% url 'chrono-manager-agenda-pricing-add' agenda.pk %}">{% trans "New pricing" %}</a>
{% endif %}
{% endblock %}
{% block appbar %}
{% if object.pk %}
<h2>{% trans "Edit pricing" %}</h2>
{% else %}
<h2>{% trans "New pricing" %}</h2>
{% endif %}
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
{% if object.pk %}
<a class="cancel" href="{% url 'chrono-manager-agenda-pricing-detail' agenda.pk object.pk %}">{% trans 'Cancel' %}</a>
{% else %}
<a class="cancel" href="{% url 'chrono-manager-agenda-settings' agenda.pk %}">{% trans 'Cancel' %}</a>
{% endif %}
</div>
</form>
{% endblock %}

View File

@ -111,4 +111,24 @@ urlpatterns = [
views.criteria_delete,
name='chrono-manager-pricing-criteria-delete',
),
url(
r'^agenda/(?P<pk>\d+)/pricing/add/$',
views.agenda_pricing_add,
name='chrono-manager-agenda-pricing-add',
),
url(
r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/$',
views.agenda_pricing_detail,
name='chrono-manager-agenda-pricing-detail',
),
url(
r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/edit/$',
views.agenda_pricing_edit,
name='chrono-manager-agenda-pricing-edit',
),
url(
r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/delete/$',
views.agenda_pricing_delete,
name='chrono-manager-agenda-pricing-delete',
),
]

View File

@ -25,14 +25,17 @@ from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views.generic import CreateView, DeleteView, DetailView, FormView, ListView, UpdateView
from chrono.agendas.models import Agenda
from chrono.manager.views import ManagedAgendaMixin, ViewableAgendaMixin
from chrono.pricing.forms import (
AgendaPricingForm,
CriteriaForm,
NewCriteriaForm,
PricingCriteriaCategoryAddForm,
PricingCriteriaCategoryEditForm,
PricingVariableFormSet,
)
from chrono.pricing.models import Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
from chrono.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
class PricingListView(ListView):
@ -487,3 +490,68 @@ class CriteriaDeleteView(DeleteView):
criteria_delete = CriteriaDeleteView.as_view()
class AgendaPricingAddView(ManagedAgendaMixin, CreateView):
template_name = 'chrono/pricing/manager_agenda_pricing_form.html'
model = AgendaPricing
form_class = AgendaPricingForm
def get_success_url(self):
return reverse('chrono-manager-agenda-pricing-detail', args=[self.agenda.pk, self.object.pk])
agenda_pricing_add = AgendaPricingAddView.as_view()
class AgendaPricingDetailView(ViewableAgendaMixin, DetailView):
model = AgendaPricing
pk_url_kwarg = 'pricing_pk'
template_name = 'chrono/pricing/manager_agenda_pricing_detail.html'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_queryset(self):
return AgendaPricing.objects.filter(agenda=self.agenda)
def get_context_data(self, **kwargs):
kwargs['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
return super().get_context_data(**kwargs)
agenda_pricing_detail = AgendaPricingDetailView.as_view()
class AgendaPricingEditView(ManagedAgendaMixin, UpdateView):
template_name = 'chrono/pricing/manager_agenda_pricing_form.html'
model = AgendaPricing
pk_url_kwarg = 'pricing_pk'
form_class = AgendaPricingForm
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_queryset(self):
return AgendaPricing.objects.filter(agenda=self.agenda)
def get_success_url(self):
return reverse('chrono-manager-agenda-pricing-detail', args=[self.agenda.pk, self.object.pk])
agenda_pricing_edit = AgendaPricingEditView.as_view()
class AgendaPricingDeleteView(ManagedAgendaMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = AgendaPricing
pk_url_kwarg = 'pricing_pk'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_queryset(self):
return AgendaPricing.objects.filter(agenda=self.agenda)
agenda_pricing_delete = AgendaPricingDeleteView.as_view()

View File

@ -25,6 +25,7 @@ from chrono.agendas.models import (
UnavailabilityCalendar,
VirtualMember,
)
from chrono.pricing.models import AgendaPricing, Pricing
from chrono.utils.signature import check_query
from tests.utils import login
@ -444,6 +445,56 @@ def test_options_events_agenda_events_type(app, admin_user):
assert 'events_type' not in resp.context['form'].fields
def test_options_events_agenda_pricing(settings, app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
pricing1 = Pricing.objects.create(label='Model 1')
pricing2 = Pricing.objects.create(label='Model 2')
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
assert 'Pricing' in resp
assert 'Model 1' not in resp
assert 'Model 2' not in resp
AgendaPricing.objects.create(
agenda=agenda,
pricing=pricing1,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
AgendaPricing.objects.create(
agenda=agenda,
pricing=pricing1,
date_start=datetime.date(year=2022, month=9, day=1),
date_end=datetime.date(year=2023, month=9, day=1),
)
AgendaPricing.objects.create(
agenda=agenda,
pricing=pricing2,
date_start=datetime.date(year=2022, month=9, day=1),
date_end=datetime.date(year=2023, month=9, day=1),
)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
assert 'Model 1 (01/09/2021 - 01/09/2022)' in resp
assert 'Model 1 (01/09/2022 - 01/09/2023)' in resp
assert 'Model 2 (01/09/2022 - 01/09/2023)' in resp
settings.CHRONO_ENABLE_PRICING = False
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
assert 'Pricing' not in resp
assert 'Model 1' not in resp
assert 'Model 2' not in resp
settings.CHRONO_ENABLE_PRICING = True
# check kind
agenda.kind = 'meetings'
agenda.save()
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
assert 'Pricing' not in resp
assert 'Model 1' not in resp
assert 'Model 2' not in resp
def test_options_events_agenda_delays(settings, app, admin_user):
settings.WORKING_DAY_CALENDAR = None
agenda = Agenda.objects.create(label='Foo bar')

View File

@ -1,11 +1,12 @@
import copy
import datetime
import json
import pytest
from webtest import Upload
from chrono.agendas.models import Agenda
from chrono.pricing.models import Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
from chrono.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
from tests.utils import login
pytestmark = pytest.mark.django_db
@ -765,3 +766,215 @@ def test_import_criteria_category(app, admin_user):
)
assert CriteriaCategory.objects.count() == 3
assert Criteria.objects.count() == 6
def test_add_agenda_pricing(settings, app, admin_user):
agenda = Agenda.objects.create(label='Foo Bar')
pricing = Pricing.objects.create(label='Model')
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('New pricing')
resp.form['pricing'] = pricing.pk
resp.form['date_start'] = '2021-09-01'
resp.form['date_end'] = '2021-09-01'
resp = resp.form.submit()
assert resp.context['form'].errors['date_end'] == ['End date must be greater than start date.']
resp.form['date_end'] = '2022-09-01'
resp = resp.form.submit()
agenda_pricing = AgendaPricing.objects.latest('pk')
assert resp.location.endswith('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
assert agenda_pricing.pricing == pricing
assert agenda_pricing.agenda == agenda
assert agenda_pricing.date_start == datetime.date(2021, 9, 1)
assert agenda_pricing.date_end == datetime.date(2022, 9, 1)
resp = app.get('/manage/pricing/agenda/%s/pricing/add/' % agenda.pk)
resp.form['pricing'] = pricing.pk
resp.form['date_start'] = '2021-11-01'
resp.form['date_end'] = '2022-11-01'
resp = resp.form.submit()
assert resp.context['form'].errors['__all__'] == ['Pricing overlaps existing pricings.']
resp.form['date_start'] = '2022-09-01'
resp.form['date_end'] = '2023-09-01'
resp = resp.form.submit()
agenda_pricing = AgendaPricing.objects.latest('pk')
assert agenda_pricing.pricing == pricing
assert agenda_pricing.agenda == agenda
assert agenda_pricing.date_start == datetime.date(2022, 9, 1)
assert agenda_pricing.date_end == datetime.date(2023, 9, 1)
settings.CHRONO_ENABLE_PRICING = False
resp = app.get('/manage/')
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
assert 'New pricing' not in resp
def test_add_agenda_pricing_as_manager(app, manager_user, agenda_with_restrictions):
app = login(app, username='manager', password='manager')
app.get('/manage/pricing/agenda/%s/pricing/add/' % agenda_with_restrictions.pk, status=403)
def test_edit_agenda_pricing(app, admin_user):
agenda = Agenda.objects.create(label='Foo Bar')
pricing = Pricing.objects.create(label='Model')
pricing2 = Pricing.objects.create(label='Model 2')
agenda_pricing = AgendaPricing.objects.create(
agenda=agenda,
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda2 = Agenda.objects.create(label='Foo Bar')
agenda_pricing2 = AgendaPricing.objects.create(
agenda=agenda2,
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
AgendaPricing.objects.create(
agenda=agenda,
pricing=pricing,
date_start=datetime.date(year=2022, month=9, day=1),
date_end=datetime.date(year=2023, month=9, day=1),
)
app = login(app)
resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
resp = resp.click(href='/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda.pk, agenda_pricing.pk))
resp.form['pricing'] = pricing2.pk
resp.form['date_start'] = '2021-09-01'
resp.form['date_end'] = '2021-09-01'
resp = resp.form.submit()
assert resp.context['form'].errors['date_end'] == ['End date must be greater than start date.']
resp.form['date_start'] = '2021-11-01'
resp.form['date_end'] = '2022-11-01'
resp = resp.form.submit()
assert resp.context['form'].errors['__all__'] == ['Pricing overlaps existing pricings.']
resp.form['date_start'] = '2021-08-01'
resp.form['date_end'] = '2022-09-01'
resp = resp.form.submit()
assert resp.location.endswith('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
agenda_pricing.refresh_from_db()
assert agenda_pricing.pricing == pricing2
assert agenda_pricing.agenda == agenda
assert agenda_pricing.date_start == datetime.date(2021, 8, 1)
assert agenda_pricing.date_end == datetime.date(2022, 9, 1)
app.get('/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda.pk, agenda_pricing2.pk), status=404)
app.get('/manage/pricing/agenda/%s/pricing/%s/edit/' % (0, agenda_pricing.pk), status=404)
app.get('/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda.pk, 0), status=404)
# wrong kind
for kind in ['meetings', 'virtual']:
agenda.kind = kind
agenda.save()
app.get('/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda.pk, agenda_pricing.pk), status=404)
def test_edit_agenda_pricing_as_manager(app, manager_user, agenda_with_restrictions):
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
agenda=agenda_with_restrictions,
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
app = login(app, username='manager', password='manager')
app.get(
'/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda_with_restrictions.pk, agenda_pricing.pk),
status=403,
)
resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda_with_restrictions.pk, agenda_pricing.pk))
assert (
'/manage/pricing/agenda/%s/pricing/%s/edit/' % (agenda_with_restrictions.pk, agenda_pricing.pk)
not in resp
)
def test_delete_agenda_pricing_as_manager(app, manager_user, agenda_with_restrictions):
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
agenda=agenda_with_restrictions,
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
app = login(app, username='manager', password='manager')
app.get(
'/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda_with_restrictions.pk, agenda_pricing.pk),
status=403,
)
resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda_with_restrictions.pk, agenda_pricing.pk))
assert (
'/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda_with_restrictions.pk, agenda_pricing.pk)
not in resp
)
def test_delete_agenda_pricing(app, admin_user):
agenda = Agenda.objects.create(label='Foo Bar')
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
agenda=agenda,
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
agenda2 = Agenda.objects.create(label='Foo Bar')
agenda_pricing2 = AgendaPricing.objects.create(
agenda=agenda2,
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
app = login(app)
app.get('/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda.pk, agenda_pricing2.pk), status=404)
app.get('/manage/pricing/agenda/%s/pricing/%s/delete/' % (0, agenda_pricing.pk), status=404)
app.get('/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda.pk, 0), status=404)
# wrong kind
for kind in ['meetings', 'virtual']:
agenda.kind = kind
agenda.save()
app.get('/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda.pk, agenda_pricing.pk), status=404)
agenda.kind = 'events'
agenda.save()
resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
resp = resp.click(href='/manage/pricing/agenda/%s/pricing/%s/delete/' % (agenda.pk, agenda_pricing.pk))
resp = resp.form.submit()
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.pk)
assert AgendaPricing.objects.filter(pk=agenda_pricing.pk).exists() is False
def test_detail_agenda_pricing(app, admin_user):
agenda = Agenda.objects.create(label='Foo Bar')
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
agenda=agenda,
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
agenda2 = Agenda.objects.create(label='Foo Bar')
agenda_pricing2 = AgendaPricing.objects.create(
agenda=agenda2,
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click(href='/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing2.pk), status=404)
app.get('/manage/pricing/agenda/%s/pricing/%s/' % (0, agenda_pricing.pk), status=404)
app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, 0), status=404)
# wrong kind
for kind in ['meetings', 'virtual']:
agenda.kind = kind
agenda.save()
app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk), status=404)