diff --git a/lingo/pricing/forms.py b/lingo/pricing/forms.py
index d8a7061..881c24f 100644
--- a/lingo/pricing/forms.py
+++ b/lingo/pricing/forms.py
@@ -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')
diff --git a/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_billing_date_form.html b/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_billing_date_form.html
new file mode 100644
index 0000000..4fb6f60
--- /dev/null
+++ b/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_billing_date_form.html
@@ -0,0 +1,30 @@
+{% extends "lingo/pricing/manager_agenda_pricing_detail.html" %}
+{% load i18n %}
+
+{% block breadcrumb %}
+{{ block.super }}
+{% if form.instance.pk %}
+{{ form.instance }}
+{% else %}
+{% trans "New billing date" %}
+{% endif %}
+{% endblock %}
+
+{% block appbar %}
+{% if form.instance.pk %}
+
{{ form.instance.agenda_pricing }} - {% trans "Edit billing date" %}
+{% else %}
+{{ form.instance.agenda_pricing }} - {% trans "New billing date" %}
+{% endif %}
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html b/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
index 35bed3d..1feff4b 100644
--- a/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
+++ b/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
@@ -31,6 +31,9 @@
{% trans "Agendas" %}
{% endif %}
{% trans "Test tool" %}
+ {% if object.flat_fee_schedule %}
+ {% trans "Billing dates" %}
+ {% endif %}
{% for matrix in iter_matrix %}
{% trans "Pricings" context 'amount' %}{% if matrix.criteria %} - {{ matrix.criteria.label }}{% endif %}
{% empty %}
@@ -119,10 +122,36 @@
{{ pricing_data|pprint }}
- {% endwith %}
- {% endif %}
+ {% endwith %}
+ {% endif %}
+ {% if object.flat_fee_schedule %}
+
+ {% if billing_dates %}
+
+ {% else %}
+
+ {% 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 %}
+
+ {% endif %}
+
+
+ {% endif %}
+
{% for matrix in iter_matrix %}
diff --git a/lingo/pricing/urls.py b/lingo/pricing/urls.py
index 985862c..f10b570 100644
--- a/lingo/pricing/urls.py
+++ b/lingo/pricing/urls.py
@@ -198,6 +198,21 @@ urlpatterns = [
views.agenda_pricing_agenda_delete,
name='lingo-manager-agenda-pricing-agenda-delete',
),
+ url(
+ r'^agenda-pricing/(?P\d+)/billing-date/add/$',
+ views.agenda_pricing_billing_date_add,
+ name='lingo-manager-agenda-pricing-billing-date-add',
+ ),
+ url(
+ r'^agenda-pricing/(?P\d+)/billing-date/(?P\d+)/$',
+ views.agenda_pricing_billing_date_edit,
+ name='lingo-manager-agenda-pricing-billing-date-edit',
+ ),
+ url(
+ r'^agenda-pricing/(?P\d+)/billing-date/(?P\d+)/delete/$',
+ views.agenda_pricing_billing_date_delete,
+ name='lingo-manager-agenda-pricing-billing-date-delete',
+ ),
url(
r'^agenda-pricing/(?P\d+)/matrix/edit/$',
views.agenda_pricing_matrix_edit,
diff --git a/lingo/pricing/views.py b/lingo/pricing/views.py
index 4166d5d..b111554 100644
--- a/lingo/pricing/views.py
+++ b/lingo/pricing/views.py
@@ -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'
diff --git a/tests/pricing/manager/test_agenda_pricing.py b/tests/pricing/manager/test_agenda_pricing.py
index b48e73f..02c9c1c 100644
--- a/tests/pricing/manager/test_agenda_pricing.py
+++ b/tests/pricing/manager/test_agenda_pricing.py
@@ -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)