pricing: billing date views (#67675)

This commit is contained in:
Lauréline Guérin 2022-07-26 15:55:17 +02:00
parent a179e2c0ff
commit fc7dc98b1f
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
6 changed files with 427 additions and 5 deletions

View File

@ -25,7 +25,7 @@ from django.utils.translation import ugettext_lazy as _
from lingo.agendas.chrono import ChronoError, get_event, get_subscriptions
from lingo.agendas.models import Agenda, CheckType
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, PricingError
from lingo.pricing.models import AgendaPricing, BillingDate, Criteria, CriteriaCategory, PricingError
class ExportForm(forms.Form):
@ -219,6 +219,25 @@ class AgendaPricingForm(NewAgendaPricingForm):
None,
_('Agenda "%s" has already a pricing overlapping this period.') % agenda.label,
)
if (
old_flat_fee_schedule != new_flat_fee_schedule
and new_flat_fee_schedule is False
and self.instance.billingdates.exists()
):
self.add_error(
'flat_fee_schedule',
_('Some billing dates are are defined for this pricing; please delete them first.'),
)
if (
old_flat_fee_schedule == new_flat_fee_schedule
and new_flat_fee_schedule is True
and (old_date_start != new_date_start or old_date_end != new_date_end)
):
if (
self.instance.billingdates.filter(date_start__lt=new_date_start).exists()
or self.instance.billingdates.filter(date_start__gte=new_date_end).exists()
):
self.add_error(None, _('Some billing dates are outside the pricing period.'))
return cleaned_data
@ -244,6 +263,24 @@ class AgendaPricingAgendaAddForm(forms.Form):
return agenda
class AgendaPricingBillingDateForm(forms.ModelForm):
class Meta:
model = BillingDate
fields = ['date_start', 'label']
widgets = {
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
}
def clean_date_start(self):
date_start = self.cleaned_data['date_start']
if (
date_start < self.instance.agenda_pricing.date_start
or date_start >= self.instance.agenda_pricing.date_end
):
raise forms.ValidationError(_('The billing start date must be within the period of the pricing.'))
return date_start
class PricingMatrixForm(forms.Form):
def __init__(self, *args, **kwargs):
matrix = kwargs.pop('matrix')

View File

@ -0,0 +1,30 @@
{% extends "lingo/pricing/manager_agenda_pricing_detail.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
{% if form.instance.pk %}
<a href="{% url 'lingo-manager-agenda-pricing-billing-date-edit' object.pk form.instance.agenda_pricing.pk %}">{{ form.instance }}</a>
{% else %}
<a href="{% url 'lingo-manager-agenda-pricing-billing-date-add' object.pk %}">{% trans "New billing date" %}</a>
{% endif %}
{% endblock %}
{% block appbar %}
{% if form.instance.pk %}
<h2>{{ form.instance.agenda_pricing }} - {% trans "Edit billing date" %}</h2>
{% else %}
<h2>{{ form.instance.agenda_pricing }} - {% trans "New billing date" %}</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>
<a class="cancel" href="{% url 'lingo-manager-agenda-pricing-detail' object.pk %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -31,6 +31,9 @@
<button aria-controls="panel-agendas" aria-selected="false" id="tab-agendas" role="tab" tabindex="0">{% trans "Agendas" %}</button>
{% endif %}
<button aria-controls="panel-debug" aria-selected="false" id="tab-debug" role="tab" tabindex="0">{% trans "Test tool" %}</button>
{% if object.flat_fee_schedule %}
<button aria-controls="panel-billing-dates" aria-selected="false" id="tab-billing-dates" role="tab" tabindex="0">{% trans "Billing dates" %}</button>
{% endif %}
{% for matrix in iter_matrix %}
<button aria-controls="panel-matrix-{{ matrix.criteria.slug }}" aria-selected="false" id="tab-matrix-{{ matrix.criteria.slug }}" role="tab" tabindex="-1">{% trans "Pricings" context 'amount' %}{% if matrix.criteria %} - {{ matrix.criteria.label }}{% endif %}</button>
{% empty %}
@ -119,10 +122,36 @@
<pre>{{ pricing_data|pprint }}</pre>
</div>
</div>
{% endwith %}
{% endif %}
{% endwith %}
{% endif %}
</div>
{% if object.flat_fee_schedule %}
<div aria-labelledby="tab-billing-dates" hidden id="panel-billing-dates" role="tabpanel" tabindex="0">
{% if billing_dates %}
<ul class="objects-list single-links">
{% for billing_date in billing_dates %}
<li>
<a rel="popup" href="{% url 'lingo-manager-agenda-pricing-billing-date-edit' object.pk billing_date.pk %}">
{{ billing_date.date_start|date:'d/m/Y' }} ({{ billing_date.label }})
</a>
<a class="delete" rel="popup" href="{% url 'lingo-manager-agenda-pricing-billing-date-delete' object.pk billing_date.pk %}">{% trans "remove"%}</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="big-msg-info">
{% blocktrans with date_start=object.date_start|date:'d/m/Y' %}
No billing dates. The start date of the pricing ({{ date_start }}) is used as the only available billing date.
{% endblocktrans %}
</div>
{% endif %}
<div class="panel--buttons">
<a class="pk-button" rel="popup" href="{% url 'lingo-manager-agenda-pricing-billing-date-add' object.pk %}">{% trans "New billing date" %}</a>
</div>
</div>
{% endif %}
{% for matrix in iter_matrix %}
<div aria-labelledby="tab-matrix-{{ matrix.criteria.slug }}" hidden="" id="panel-matrix-{{ matrix.criteria.slug }}" role="tabpanel" tabindex="0">
<table class="main pricing-matrix-{{ matrix.criteria.slug }}">

View File

@ -198,6 +198,21 @@ urlpatterns = [
views.agenda_pricing_agenda_delete,
name='lingo-manager-agenda-pricing-agenda-delete',
),
url(
r'^agenda-pricing/(?P<pk>\d+)/billing-date/add/$',
views.agenda_pricing_billing_date_add,
name='lingo-manager-agenda-pricing-billing-date-add',
),
url(
r'^agenda-pricing/(?P<pk>\d+)/billing-date/(?P<billing_date_pk>\d+)/$',
views.agenda_pricing_billing_date_edit,
name='lingo-manager-agenda-pricing-billing-date-edit',
),
url(
r'^agenda-pricing/(?P<pk>\d+)/billing-date/(?P<billing_date_pk>\d+)/delete/$',
views.agenda_pricing_billing_date_delete,
name='lingo-manager-agenda-pricing-billing-date-delete',
),
url(
r'^agenda-pricing/(?P<pk>\d+)/matrix/edit/$',
views.agenda_pricing_matrix_edit,

View File

@ -45,6 +45,7 @@ from lingo.agendas.models import Agenda, CheckType, CheckTypeGroup
from lingo.agendas.views import AgendaMixin
from lingo.pricing.forms import (
AgendaPricingAgendaAddForm,
AgendaPricingBillingDateForm,
AgendaPricingForm,
CheckTypeForm,
CriteriaForm,
@ -60,7 +61,14 @@ from lingo.pricing.forms import (
PricingTestToolForm,
PricingVariableFormSet,
)
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
from lingo.pricing.models import (
AgendaPricing,
BillingDate,
Criteria,
CriteriaCategory,
Pricing,
PricingCriteriaCategory,
)
from lingo.pricing.utils import export_site, import_site
from lingo.utils.misc import AgendaImportError
@ -780,6 +788,7 @@ class AgendaPricingDetailView(DetailView):
if self.request.GET:
form.is_valid()
kwargs['test_tool_form'] = form
kwargs['billing_dates'] = self.object.billingdates.order_by('date_start')
return super().get_context_data(**kwargs)
@ -892,6 +901,92 @@ class AgendaPricingAgendaDeleteView(DeleteView):
agenda_pricing_agenda_delete = AgendaPricingAgendaDeleteView.as_view()
class AgendaPricingBillingDateAddView(FormView):
template_name = 'lingo/pricing/manager_agenda_pricing_billing_date_form.html'
model = AgendaPricing
form_class = AgendaPricingBillingDateForm
def dispatch(self, request, *args, **kwargs):
self.object = get_object_or_404(AgendaPricing, pk=kwargs['pk'], flat_fee_schedule=True)
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['instance'] = BillingDate(agenda_pricing=self.object)
return kwargs
def get_context_data(self, **kwargs):
kwargs['object'] = self.object
return super().get_context_data(**kwargs)
def form_valid(self, form):
form.save()
return super().form_valid(form)
def get_success_url(self):
return '%s#open:billing-dates' % reverse('lingo-manager-agenda-pricing-detail', args=[self.object.pk])
agenda_pricing_billing_date_add = AgendaPricingBillingDateAddView.as_view()
class AgendaPricingBillingDateEditView(FormView):
template_name = 'lingo/pricing/manager_agenda_pricing_billing_date_form.html'
model = AgendaPricing
form_class = AgendaPricingBillingDateForm
def dispatch(self, request, *args, **kwargs):
self.agenda_pricing = get_object_or_404(AgendaPricing, pk=kwargs['pk'], flat_fee_schedule=True)
self.object = get_object_or_404(self.agenda_pricing.billingdates, pk=kwargs['billing_date_pk'])
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['instance'] = self.object
return kwargs
def get_context_data(self, **kwargs):
kwargs['object'] = self.agenda_pricing
return super().get_context_data(**kwargs)
def form_valid(self, form):
form.save()
return super().form_valid(form)
def get_success_url(self):
return '%s#open:billing-dates' % reverse(
'lingo-manager-agenda-pricing-detail', args=[self.agenda_pricing.pk]
)
agenda_pricing_billing_date_edit = AgendaPricingBillingDateEditView.as_view()
class AgendaPricingBillingDateDeleteView(DeleteView):
template_name = 'lingo/manager_confirm_delete.html'
model = BillingDate
pk_url_kwarg = 'billing_date_pk'
def dispatch(self, request, *args, **kwargs):
self.agenda_pricing = get_object_or_404(AgendaPricing, pk=kwargs['pk'], subscription_required=True)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.agenda_pricing.billingdates.all()
def delete(self, request, *args, **kwargs):
self.get_object().delete()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return '%s#open:billing-dates' % reverse(
'lingo-manager-agenda-pricing-detail', args=[self.agenda_pricing.pk]
)
agenda_pricing_billing_date_delete = AgendaPricingBillingDateDeleteView.as_view()
class AgendaPricingMatrixEdit(FormView):
template_name = 'lingo/pricing/manager_agenda_pricing_matrix_form.html'

View File

@ -7,7 +7,7 @@ from django.utils.timezone import now
from lingo.agendas.chrono import ChronoError
from lingo.agendas.models import Agenda, CheckType, CheckTypeGroup
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingError
from lingo.pricing.models import AgendaPricing, BillingDate, Criteria, CriteriaCategory, Pricing, PricingError
from tests.utils import login
pytestmark = pytest.mark.django_db
@ -124,6 +124,19 @@ def test_edit_agenda_pricing(app, admin_user):
resp = resp.form.submit()
assert resp.location.endswith('/manage/pricing/agenda-pricing/%s/' % agenda_pricing.pk)
agenda_pricing.flat_fee_schedule = True
agenda_pricing.save()
agenda_pricing.billingdates.create(
date_start=datetime.date(year=2021, month=9, day=1),
label='Foo',
)
resp = app.get('/manage/pricing/agenda-pricing/%s/edit/' % agenda_pricing.pk)
resp.form['flat_fee_schedule'] = False
resp = resp.form.submit()
assert resp.context['form'].errors['flat_fee_schedule'] == [
'Some billing dates are are defined for this pricing; please delete them first.'
]
def test_edit_agenda_pricing_overlapping_flat_fee_schedule(app, admin_user):
agenda1 = Agenda.objects.create(label='Foo bar 1')
@ -263,6 +276,80 @@ def test_edit_agenda_pricing_overlapping_date_end(app, admin_user):
resp.form.submit().follow()
def test_edit_agenda_pricing_billing_date_start(app, admin_user):
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
label='Foo Bar',
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
flat_fee_schedule=True,
)
agenda_pricing.billingdates.create(
date_start=datetime.date(year=2021, month=9, day=1),
label='Foo',
)
app = login(app)
resp = app.get('/manage/pricing/agenda-pricing/%s/edit/' % agenda_pricing.pk)
# ok, date_start has not changed
resp.form.submit().follow()
resp = app.get('/manage/pricing/agenda-pricing/%s/edit/' % agenda_pricing.pk)
# ok, billing date inside period
resp.form['date_start'] = '2021-08-31'
resp.form.submit().follow()
resp = app.get('/manage/pricing/agenda-pricing/%s/edit/' % agenda_pricing.pk)
resp.form['date_start'] = '2021-09-02'
resp = resp.form.submit()
assert resp.context['form'].non_field_errors() == ['Some billing dates are outside the pricing period.']
# but don't check billing dates if not flat_fee_schedule
agenda_pricing.flat_fee_schedule = False
agenda_pricing.save()
# ok
resp = app.get('/manage/pricing/agenda-pricing/%s/edit/' % agenda_pricing.pk)
resp.form.submit().follow()
def test_edit_agenda_pricing_billing_date_end(app, admin_user):
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
label='Foo Bar',
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
flat_fee_schedule=True,
)
agenda_pricing.billingdates.create(
date_start=datetime.date(year=2022, month=9, day=1),
label='Foo',
)
app = login(app)
resp = app.get('/manage/pricing/agenda-pricing/%s/edit/' % agenda_pricing.pk)
# ok, date_end has not changed
resp.form.submit().follow()
resp = app.get('/manage/pricing/agenda-pricing/%s/edit/' % agenda_pricing.pk)
# ok, billing date inside period
resp.form['date_end'] = '2022-09-02'
resp.form.submit().follow()
resp = app.get('/manage/pricing/agenda-pricing/%s/edit/' % agenda_pricing.pk)
resp.form['date_end'] = '2022-09-01'
resp = resp.form.submit()
assert resp.context['form'].non_field_errors() == ['Some billing dates are outside the pricing period.']
# but don't check billing dates if not flat_fee_schedule
agenda_pricing.flat_fee_schedule = False
agenda_pricing.save()
# ok
resp = app.get('/manage/pricing/agenda-pricing/%s/edit/' % agenda_pricing.pk)
resp.form.submit().follow()
def test_detail_agenda_pricing(app, admin_user):
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
@ -402,6 +489,135 @@ def test_agenda_pricing_delete_agenda(app, admin_user):
)
def test_agenda_pricing_add_billing_date(app, admin_user):
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
flat_fee_schedule=True,
)
app = login(app)
resp = app.get('/manage/pricing/agenda-pricing/%s/' % agenda_pricing.pk)
resp = resp.click(href='/manage/pricing/agenda-pricing/%s/billing-date/add/' % agenda_pricing.pk)
resp.form['date_start'] = '2021-08-31'
resp.form['label'] = 'Foo'
resp = resp.form.submit()
assert resp.context['form'].errors['date_start'] == [
'The billing start date must be within the period of the pricing.'
]
resp.form['date_start'] = '2022-09-01'
resp = resp.form.submit()
assert resp.context['form'].errors['date_start'] == [
'The billing start date must be within the period of the pricing.'
]
resp.form['date_start'] = '2022-08-31'
resp = resp.form.submit()
assert resp.location.endswith('/manage/pricing/agenda-pricing/%s/#open:billing-dates' % agenda_pricing.pk)
assert agenda_pricing.billingdates.count() == 1
billing_date = BillingDate.objects.latest('pk')
assert billing_date.date_start == datetime.date(2022, 8, 31)
assert billing_date.label == 'Foo'
agenda_pricing.flat_fee_schedule = False
agenda_pricing.save()
app.get('/manage/pricing/agenda-pricing/%s/billing-date/add/' % agenda_pricing.pk, status=404)
def test_agenda_pricing_edit_billing_date(app, admin_user):
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
flat_fee_schedule=True,
)
billing_date = agenda_pricing.billingdates.create(
date_start=datetime.date(year=2021, month=8, day=31),
label='Foo',
)
app = login(app)
resp = app.get('/manage/pricing/agenda-pricing/%s/' % agenda_pricing.pk)
resp = resp.click(
href=r'/manage/pricing/agenda-pricing/%s/billing-date/%s/$' % (agenda_pricing.pk, billing_date.pk)
)
resp.form['label'] = 'Bar'
resp = resp.form.submit()
assert resp.context['form'].errors['date_start'] == [
'The billing start date must be within the period of the pricing.'
]
resp.form['date_start'] = '2022-09-01'
resp = resp.form.submit()
assert resp.context['form'].errors['date_start'] == [
'The billing start date must be within the period of the pricing.'
]
resp.form['date_start'] = '2022-08-31'
resp = resp.form.submit()
assert resp.location.endswith('/manage/pricing/agenda-pricing/%s/#open:billing-dates' % agenda_pricing.pk)
assert agenda_pricing.billingdates.count() == 1
billing_date.refresh_from_db()
assert billing_date.date_start == datetime.date(2022, 8, 31)
assert billing_date.label == 'Bar'
agenda_pricing.flat_fee_schedule = False
agenda_pricing.save()
app.get(
'/manage/pricing/agenda-pricing/%s/billing-date/%s/' % (agenda_pricing.pk, billing_date.pk),
status=404,
)
def test_agenda_pricing_delete_billing_date(app, admin_user):
pricing = Pricing.objects.create(label='Model')
agenda_pricing = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
flat_fee_schedule=True,
)
billing_date1 = agenda_pricing.billingdates.create(
date_start=datetime.date(year=2021, month=9, day=1),
label='Foo',
)
billing_date2 = agenda_pricing.billingdates.create(
date_start=datetime.date(year=2021, month=9, day=1),
label='Bar',
)
app = login(app)
resp = app.get('/manage/pricing/agenda-pricing/%s/' % agenda_pricing.pk)
resp = resp.click(
href='/manage/pricing/agenda-pricing/%s/billing-date/%s/delete'
% (agenda_pricing.pk, billing_date1.pk)
)
resp = resp.form.submit()
assert resp.location.endswith('/manage/pricing/agenda-pricing/%s/#open:billing-dates' % agenda_pricing.pk)
assert agenda_pricing.billingdates.count() == 1
agenda_pricing.flat_fee_schedule = False
agenda_pricing.save()
app.get(
'/manage/pricing/agenda-pricing/%s/billing-date/%s/' % (agenda_pricing.pk, billing_date2.pk),
status=404,
)
agenda_pricing2 = AgendaPricing.objects.create(
pricing=pricing,
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2022, month=9, day=1),
)
app.get(
'/manage/pricing/agenda-pricing/%s/billing-date/%s/' % (agenda_pricing2.pk, billing_date2.pk),
status=404,
)
app.get('/manage/pricing/agenda-pricing/%s/billing-date/%s/' % (0, billing_date2.pk), status=404)
def test_detail_agenda_pricing_3_categories(app, admin_user):
category1 = CriteriaCategory.objects.create(label='Cat 1')
Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)