1247 lines
41 KiB
Python
1247 lines
41 KiB
Python
# lingo - payment and billing system
|
|
# Copyright (C) 2022 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import datetime
|
|
import json
|
|
from collections import defaultdict
|
|
from operator import itemgetter
|
|
|
|
from django import forms
|
|
from django.contrib import messages
|
|
from django.db.models import Prefetch
|
|
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
|
|
from django.shortcuts import get_object_or_404
|
|
from django.urls import reverse, reverse_lazy
|
|
from django.utils.encoding import force_str
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils.translation import ngettext
|
|
from django.views.generic import (
|
|
CreateView,
|
|
DeleteView,
|
|
DetailView,
|
|
FormView,
|
|
ListView,
|
|
RedirectView,
|
|
TemplateView,
|
|
UpdateView,
|
|
)
|
|
from django.views.generic.detail import SingleObjectMixin
|
|
|
|
from lingo.agendas.chrono import refresh_agendas
|
|
from lingo.agendas.models import Agenda, CheckType, CheckTypeGroup
|
|
from lingo.agendas.views import AgendaMixin
|
|
from lingo.pricing.forms import (
|
|
AgendaPricingAgendaAddForm,
|
|
AgendaPricingBillingDateForm,
|
|
AgendaPricingForm,
|
|
CheckTypeForm,
|
|
CriteriaForm,
|
|
ExportForm,
|
|
ImportForm,
|
|
NewAgendaPricingForm,
|
|
NewCheckTypeForm,
|
|
NewCriteriaForm,
|
|
PricingCriteriaCategoryAddForm,
|
|
PricingCriteriaCategoryEditForm,
|
|
PricingDuplicateForm,
|
|
PricingMatrixForm,
|
|
PricingPayerFormSet,
|
|
PricingTestToolForm,
|
|
PricingVariableFormSet,
|
|
)
|
|
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 LingoImportError
|
|
|
|
|
|
class HomeView(TemplateView):
|
|
template_name = 'lingo/pricing/manager_home.html'
|
|
|
|
|
|
home = HomeView.as_view()
|
|
|
|
|
|
class ConfigExportView(FormView):
|
|
form_class = ExportForm
|
|
template_name = 'lingo/pricing/export.html'
|
|
|
|
def form_valid(self, form):
|
|
response = HttpResponse(content_type='application/json')
|
|
today = datetime.date.today()
|
|
response['Content-Disposition'] = 'attachment; filename="export_pricing_config_{}.json"'.format(
|
|
today.strftime('%Y%m%d')
|
|
)
|
|
json.dump(export_site(**form.cleaned_data), response, indent=2)
|
|
return response
|
|
|
|
|
|
config_export = ConfigExportView.as_view()
|
|
|
|
|
|
class ConfigImportView(FormView):
|
|
form_class = ImportForm
|
|
template_name = 'lingo/pricing/import.html'
|
|
success_url = reverse_lazy('lingo-manager-pricing-home')
|
|
|
|
def form_valid(self, form):
|
|
try:
|
|
config_json = json.loads(force_str(self.request.FILES['config_json'].read()))
|
|
except ValueError:
|
|
form.add_error('config_json', _('File is not in the expected JSON format.'))
|
|
return self.form_invalid(form)
|
|
|
|
try:
|
|
results = import_site(config_json)
|
|
except LingoImportError as exc:
|
|
form.add_error('config_json', '%s' % exc)
|
|
return self.form_invalid(form)
|
|
except KeyError as exc:
|
|
form.add_error('config_json', _('Key "%s" is missing.') % exc.args[0])
|
|
return self.form_invalid(form)
|
|
|
|
import_messages = {
|
|
'agendas': {
|
|
'update_noop': _('No agenda updated.'),
|
|
'update': lambda x: ngettext(
|
|
'An agenda has been updated.',
|
|
'%(count)d agendas have been updated.',
|
|
x,
|
|
),
|
|
},
|
|
'check_type_groups': {
|
|
'create_noop': _('No check type group created.'),
|
|
'create': lambda x: ngettext(
|
|
'A check type group has been created.',
|
|
'%(count)d check type groups have been created.',
|
|
x,
|
|
),
|
|
'update_noop': _('No check type group updated.'),
|
|
'update': lambda x: ngettext(
|
|
'A check type group has been updated.',
|
|
'%(count)d check type groups have been updated.',
|
|
x,
|
|
),
|
|
},
|
|
'pricing_categories': {
|
|
'create_noop': _('No pricing criteria category created.'),
|
|
'create': lambda x: ngettext(
|
|
'A pricing criteria category has been created.',
|
|
'%(count)d pricing criteria categories have been created.',
|
|
x,
|
|
),
|
|
'update_noop': _('No pricing criteria category updated.'),
|
|
'update': lambda x: ngettext(
|
|
'A pricing criteria category has been updated.',
|
|
'%(count)d pricing criteria categories have been updated.',
|
|
x,
|
|
),
|
|
},
|
|
'pricing_models': {
|
|
'create_noop': _('No pricing model created.'),
|
|
'create': lambda x: ngettext(
|
|
'A pricing model has been created.',
|
|
'%(count)d pricing models have been created.',
|
|
x,
|
|
),
|
|
'update_noop': _('No pricing model updated.'),
|
|
'update': lambda x: ngettext(
|
|
'A pricing model has been updated.',
|
|
'%(count)d pricing models have been updated.',
|
|
x,
|
|
),
|
|
},
|
|
'pricings': {
|
|
'create_noop': _('No pricing created.'),
|
|
'create': lambda x: ngettext(
|
|
'A pricing has been created.',
|
|
'%(count)d pricings have been created.',
|
|
x,
|
|
),
|
|
'update_noop': _('No pricing updated.'),
|
|
'update': lambda x: ngettext(
|
|
'A pricing has been updated.',
|
|
'%(count)d pricings have been updated.',
|
|
x,
|
|
),
|
|
},
|
|
}
|
|
|
|
global_noop = True
|
|
for obj_name, obj_results in results.items():
|
|
if obj_results['all']:
|
|
global_noop = False
|
|
count = len(obj_results['created'])
|
|
if not count:
|
|
message1 = import_messages[obj_name].get('create_noop')
|
|
else:
|
|
message1 = import_messages[obj_name]['create'](count) % {'count': count}
|
|
|
|
count = len(obj_results['updated'])
|
|
if not count:
|
|
message2 = import_messages[obj_name]['update_noop']
|
|
else:
|
|
message2 = import_messages[obj_name]['update'](count) % {'count': count}
|
|
|
|
if message1:
|
|
obj_results['messages'] = "%s %s" % (message1, message2)
|
|
else:
|
|
obj_results['messages'] = message2
|
|
|
|
a_count, ct_count, pc_count, pm_count, p_count = (
|
|
len(results['agendas']['all']),
|
|
len(results['check_type_groups']['all']),
|
|
len(results['pricing_categories']['all']),
|
|
len(results['pricing_models']['all']),
|
|
len(results['pricings']['all']),
|
|
)
|
|
if (a_count, ct_count, pc_count, pm_count, p_count) == (1, 0, 0, 0, 0):
|
|
# only one agenda imported, redirect to agenda page
|
|
return HttpResponseRedirect(
|
|
reverse(
|
|
'lingo-manager-agenda-detail',
|
|
kwargs={'pk': results['agendas']['all'][0].pk},
|
|
)
|
|
)
|
|
if (a_count, ct_count, pc_count, pm_count, p_count) == (0, 1, 0, 0, 0):
|
|
# only one check type group imported, redirect to check type page
|
|
return HttpResponseRedirect(reverse('lingo-manager-check-type-list'))
|
|
if (a_count, ct_count, pc_count, pm_count, p_count) == (0, 0, 1, 0, 0):
|
|
# only one criteria category imported, redirect to criteria page
|
|
return HttpResponseRedirect(reverse('lingo-manager-pricing-criteria-list'))
|
|
if (a_count, ct_count, pc_count, pm_count, p_count) == (0, 0, 0, 1, 0):
|
|
# only one pricing model imported, redirect to pricing model page
|
|
return HttpResponseRedirect(
|
|
reverse(
|
|
'lingo-manager-pricing-detail',
|
|
kwargs={'pk': results['pricing_models']['all'][0].pk},
|
|
)
|
|
)
|
|
if (a_count, ct_count, pc_count, pm_count, p_count) == (0, 0, 0, 0, 1):
|
|
# only one pricing imported, redirect to pricing model page
|
|
return HttpResponseRedirect(
|
|
reverse(
|
|
'lingo-manager-agenda-pricing-detail',
|
|
kwargs={'pk': results['pricings']['all'][0].pk},
|
|
)
|
|
)
|
|
|
|
if global_noop:
|
|
messages.info(self.request, _('No data found.'))
|
|
else:
|
|
messages.info(self.request, results['agendas']['messages'])
|
|
messages.info(self.request, results['check_type_groups']['messages'])
|
|
messages.info(self.request, results['pricing_categories']['messages'])
|
|
messages.info(self.request, results['pricing_models']['messages'])
|
|
messages.info(self.request, results['pricings']['messages'])
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
config_import = ConfigImportView.as_view()
|
|
|
|
|
|
class PricingListView(ListView):
|
|
template_name = 'lingo/pricing/manager_pricing_list.html'
|
|
model = Pricing
|
|
|
|
|
|
pricing_list = PricingListView.as_view()
|
|
|
|
|
|
class CriteriaListView(ListView):
|
|
template_name = 'lingo/pricing/manager_criteria_list.html'
|
|
model = CriteriaCategory
|
|
|
|
def get_queryset(self):
|
|
return CriteriaCategory.objects.prefetch_related('criterias')
|
|
|
|
|
|
criteria_list = CriteriaListView.as_view()
|
|
|
|
|
|
class PricingAddView(CreateView):
|
|
template_name = 'lingo/pricing/manager_pricing_form.html'
|
|
model = Pricing
|
|
fields = ['label']
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-detail', args=[self.object.pk])
|
|
|
|
|
|
pricing_add = PricingAddView.as_view()
|
|
|
|
|
|
class PricingDetailView(DetailView):
|
|
template_name = 'lingo/pricing/manager_pricing_detail.html'
|
|
model = Pricing
|
|
|
|
def get_queryset(self):
|
|
return (
|
|
super()
|
|
.get_queryset()
|
|
.prefetch_related(
|
|
Prefetch(
|
|
'categories', queryset=CriteriaCategory.objects.order_by('pricingcriteriacategory__order')
|
|
)
|
|
)
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
kwargs['agendas'] = Agenda.objects.filter(
|
|
pk__in=AgendaPricing.objects.filter(pricing=self.object).values('agendas')
|
|
)
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
pricing_detail = PricingDetailView.as_view()
|
|
|
|
|
|
class PricingEditView(UpdateView):
|
|
template_name = 'lingo/pricing/manager_pricing_form.html'
|
|
model = Pricing
|
|
fields = ['label', 'slug']
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-detail', args=[self.object.pk])
|
|
|
|
|
|
pricing_edit = PricingEditView.as_view()
|
|
|
|
|
|
class PricingDeleteView(DeleteView):
|
|
template_name = 'lingo/manager_confirm_delete.html'
|
|
model = Pricing
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-list')
|
|
|
|
|
|
pricing_delete = PricingDeleteView.as_view()
|
|
|
|
|
|
class PricingDuplicate(SingleObjectMixin, FormView):
|
|
template_name = 'lingo/pricing/manager_pricing_duplicate_form.html'
|
|
model = Pricing
|
|
form_class = PricingDuplicateForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-detail', kwargs={'pk': self.new_pricing.pk})
|
|
|
|
def form_valid(self, form):
|
|
self.new_pricing = self.object.duplicate(label=form.cleaned_data['label'])
|
|
return super().form_valid(form)
|
|
|
|
|
|
pricing_duplicate = PricingDuplicate.as_view()
|
|
|
|
|
|
class PricingExport(DetailView):
|
|
model = Pricing
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
response = HttpResponse(content_type='application/json')
|
|
today = datetime.date.today()
|
|
attachment = 'attachment; filename="export_pricing_model_{}_{}.json"'.format(
|
|
self.get_object().slug, today.strftime('%Y%m%d')
|
|
)
|
|
response['Content-Disposition'] = attachment
|
|
json.dump({'pricing_models': [self.get_object().export_json()]}, response, indent=2)
|
|
return response
|
|
|
|
|
|
pricing_export = PricingExport.as_view()
|
|
|
|
|
|
class PricingVariableEdit(FormView):
|
|
template_name = 'lingo/pricing/manager_pricing_variable_form.html'
|
|
model = Pricing
|
|
form_class = PricingVariableFormSet
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
kwargs['object'] = self.object
|
|
return super().get_context_data(**kwargs)
|
|
|
|
def get_initial(self):
|
|
return sorted(
|
|
({'key': k, 'value': v} for k, v in self.object.extra_variables.items()),
|
|
key=itemgetter('key'),
|
|
)
|
|
|
|
def form_valid(self, form):
|
|
self.object.extra_variables = {}
|
|
for sub_data in form.cleaned_data:
|
|
if not sub_data.get('key'):
|
|
continue
|
|
self.object.extra_variables[sub_data['key']] = sub_data['value']
|
|
self.object.save()
|
|
return HttpResponseRedirect(self.get_success_url())
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-detail', args=[self.object.pk])
|
|
|
|
|
|
pricing_variable_edit = PricingVariableEdit.as_view()
|
|
|
|
|
|
class PricingPayerEdit(FormView):
|
|
template_name = 'lingo/pricing/manager_pricing_payer_form.html'
|
|
model = Pricing
|
|
form_class = PricingPayerFormSet
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
kwargs['object'] = self.object
|
|
return super().get_context_data(**kwargs)
|
|
|
|
def get_initial(self):
|
|
return list(
|
|
{'key': k, 'value': self.object.payer_variables.get(k) or ''}
|
|
for k in self.object.get_payer_variables_keys()
|
|
)
|
|
|
|
def form_valid(self, form):
|
|
self.object.payer_variables = {k: '' for k in self.object.get_payer_variables_keys()}
|
|
for sub_data in form.cleaned_data:
|
|
if not sub_data.get('key'):
|
|
continue
|
|
if sub_data['key'] not in self.object.payer_variables:
|
|
continue
|
|
self.object.payer_variables[sub_data['key']] = sub_data['value']
|
|
self.object.save()
|
|
return HttpResponseRedirect(self.get_success_url())
|
|
|
|
def get_success_url(self):
|
|
return '%s#open:payer' % reverse('lingo-manager-pricing-detail', args=[self.object.pk])
|
|
|
|
|
|
pricing_payer_edit = PricingPayerEdit.as_view()
|
|
|
|
|
|
class PricingCriteriaCategoryAddView(FormView):
|
|
template_name = 'lingo/pricing/manager_pricing_criteria_category_form.html'
|
|
model = Pricing
|
|
form_class = PricingCriteriaCategoryAddForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
|
|
if self.object.categories.count() >= 3:
|
|
raise Http404
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['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):
|
|
PricingCriteriaCategory.objects.create(pricing=self.object, category=form.cleaned_data['category'])
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return '%s#open:criterias' % reverse('lingo-manager-pricing-detail', args=[self.object.pk])
|
|
|
|
|
|
pricing_criteria_category_add = PricingCriteriaCategoryAddView.as_view()
|
|
|
|
|
|
class PricingCriteriaCategoryEditView(FormView):
|
|
template_name = 'lingo/pricing/manager_pricing_criteria_category_form.html'
|
|
model = Pricing
|
|
form_class = PricingCriteriaCategoryEditForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
|
|
self.category = get_object_or_404(self.object.categories, pk=kwargs['category_pk'])
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['pricing'] = self.object
|
|
kwargs['category'] = self.category
|
|
return kwargs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
kwargs['object'] = self.object
|
|
kwargs['category'] = self.category
|
|
return super().get_context_data(**kwargs)
|
|
|
|
def form_valid(self, form):
|
|
old_criterias = self.object.criterias.filter(category=self.category)
|
|
new_criterias = form.cleaned_data['criterias']
|
|
removed_criterias = set(old_criterias) - set(new_criterias)
|
|
self.object.criterias.remove(*removed_criterias)
|
|
self.object.criterias.add(*new_criterias)
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return '%s#open:criterias' % reverse('lingo-manager-pricing-detail', args=[self.object.pk])
|
|
|
|
|
|
pricing_criteria_category_edit = PricingCriteriaCategoryEditView.as_view()
|
|
|
|
|
|
class PricingCriteriaCategoryDeleteView(DeleteView):
|
|
template_name = 'lingo/manager_confirm_delete.html'
|
|
model = CriteriaCategory
|
|
pk_url_kwarg = 'category_pk'
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.pricing = get_object_or_404(Pricing, pk=kwargs['pk'])
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
return self.pricing.categories.all()
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
self.pricing.categories.remove(self.object)
|
|
self.pricing.criterias.remove(*self.pricing.criterias.filter(category=self.object))
|
|
return HttpResponseRedirect(self.get_success_url())
|
|
|
|
def get_success_url(self):
|
|
return '%s#open:criterias' % reverse('lingo-manager-pricing-detail', args=[self.pricing.pk])
|
|
|
|
|
|
pricing_criteria_category_delete = PricingCriteriaCategoryDeleteView.as_view()
|
|
|
|
|
|
class PricingCriteriaCategoryOrder(DetailView):
|
|
model = Pricing
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if 'new-order' not in request.GET:
|
|
return HttpResponseBadRequest('missing new-order parameter')
|
|
pricing = self.get_object()
|
|
try:
|
|
new_order = [int(x) for x in request.GET['new-order'].split(',')]
|
|
except ValueError:
|
|
return HttpResponseBadRequest('incorrect new-order parameter')
|
|
categories = pricing.categories.all()
|
|
if set(new_order) != {x.pk for x in categories} or len(new_order) != len(categories):
|
|
return HttpResponseBadRequest('incorrect new-order parameter')
|
|
for i, c_id in enumerate(new_order):
|
|
PricingCriteriaCategory.objects.filter(pricing=pricing, category=c_id).update(order=i + 1)
|
|
return HttpResponse(status=204)
|
|
|
|
|
|
pricing_criteria_category_order = PricingCriteriaCategoryOrder.as_view()
|
|
|
|
|
|
class CriteriaCategoryAddView(CreateView):
|
|
template_name = 'lingo/pricing/manager_criteria_category_form.html'
|
|
model = CriteriaCategory
|
|
fields = ['label']
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-criteria-list')
|
|
|
|
|
|
criteria_category_add = CriteriaCategoryAddView.as_view()
|
|
|
|
|
|
class CriteriaCategoryEditView(UpdateView):
|
|
template_name = 'lingo/pricing/manager_criteria_category_form.html'
|
|
model = CriteriaCategory
|
|
fields = ['label', 'slug']
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-criteria-list')
|
|
|
|
|
|
criteria_category_edit = CriteriaCategoryEditView.as_view()
|
|
|
|
|
|
class CriteriaCategoryDeleteView(DeleteView):
|
|
template_name = 'lingo/manager_confirm_delete.html'
|
|
model = CriteriaCategory
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-criteria-list')
|
|
|
|
|
|
criteria_category_delete = CriteriaCategoryDeleteView.as_view()
|
|
|
|
|
|
class CriteriaCategoryExport(DetailView):
|
|
model = CriteriaCategory
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
response = HttpResponse(content_type='application/json')
|
|
today = datetime.date.today()
|
|
attachment = 'attachment; filename="export_pricing_category_{}_{}.json"'.format(
|
|
self.get_object().slug, today.strftime('%Y%m%d')
|
|
)
|
|
response['Content-Disposition'] = attachment
|
|
json.dump({'pricing_categories': [self.get_object().export_json()]}, response, indent=2)
|
|
return response
|
|
|
|
|
|
criteria_category_export = CriteriaCategoryExport.as_view()
|
|
|
|
|
|
class CriteriaOrder(DetailView):
|
|
model = CriteriaCategory
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if 'new-order' not in request.GET:
|
|
return HttpResponseBadRequest('missing new-order parameter')
|
|
category = self.get_object()
|
|
try:
|
|
new_order = [int(x) for x in request.GET['new-order'].split(',')]
|
|
except ValueError:
|
|
return HttpResponseBadRequest('incorrect new-order parameter')
|
|
criterias = category.criterias.filter(default=False)
|
|
if set(new_order) != {x.pk for x in criterias} or len(new_order) != len(criterias):
|
|
return HttpResponseBadRequest('incorrect new-order parameter')
|
|
criterias_by_id = {c.pk: c for c in criterias}
|
|
for i, c_id in enumerate(new_order):
|
|
criterias_by_id[c_id].order = i + 1
|
|
criterias_by_id[c_id].save()
|
|
return HttpResponse(status=204)
|
|
|
|
|
|
criteria_order = CriteriaOrder.as_view()
|
|
|
|
|
|
class CriteriaAddView(CreateView):
|
|
template_name = 'lingo/pricing/manager_criteria_form.html'
|
|
model = Criteria
|
|
form_class = NewCriteriaForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.category_pk = kwargs.pop('category_pk')
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
if not kwargs.get('instance'):
|
|
kwargs['instance'] = self.model()
|
|
kwargs['instance'].category_id = self.category_pk
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-criteria-list')
|
|
|
|
|
|
criteria_add = CriteriaAddView.as_view()
|
|
|
|
|
|
class CriteriaEditView(UpdateView):
|
|
template_name = 'lingo/pricing/manager_criteria_form.html'
|
|
model = Criteria
|
|
form_class = CriteriaForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.category_pk = kwargs.pop('category_pk')
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
return Criteria.objects.filter(category=self.category_pk)
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-criteria-list')
|
|
|
|
|
|
criteria_edit = CriteriaEditView.as_view()
|
|
|
|
|
|
class CriteriaDeleteView(DeleteView):
|
|
template_name = 'lingo/manager_confirm_delete.html'
|
|
model = Criteria
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.category_pk = kwargs.pop('category_pk')
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
return Criteria.objects.filter(category=self.category_pk)
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-pricing-criteria-list')
|
|
|
|
|
|
criteria_delete = CriteriaDeleteView.as_view()
|
|
|
|
|
|
class AgendaListView(ListView):
|
|
template_name = 'lingo/pricing/manager_agenda_list.html'
|
|
model = Agenda
|
|
|
|
def get_queryset(self):
|
|
queryset = super().get_queryset()
|
|
return queryset.order_by('category_label', 'label')
|
|
|
|
|
|
agenda_list = AgendaListView.as_view()
|
|
|
|
|
|
class AgendaSyncView(RedirectView):
|
|
def get(self, request, *args, **kwargs):
|
|
refresh_agendas()
|
|
messages.info(self.request, _('Agendas refreshed.'))
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
return reverse('lingo-manager-agenda-list')
|
|
|
|
|
|
agenda_sync = AgendaSyncView.as_view()
|
|
|
|
|
|
class AgendaDetailView(AgendaMixin, DetailView):
|
|
template_name = 'lingo/pricing/manager_agenda_detail.html'
|
|
model = Agenda
|
|
|
|
def get_context_data(self, **kwargs):
|
|
kwargs['agenda_pricings'] = (
|
|
AgendaPricing.objects.filter(agendas=self.agenda, flat_fee_schedule=False)
|
|
.select_related('pricing')
|
|
.order_by('date_start', 'date_end')
|
|
)
|
|
kwargs['agenda_pricings_flat'] = (
|
|
AgendaPricing.objects.filter(agendas=self.agenda, flat_fee_schedule=True)
|
|
.select_related('pricing')
|
|
.order_by('date_start', 'date_end')
|
|
)
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
agenda_detail = AgendaDetailView.as_view()
|
|
|
|
|
|
class AgendaDetailRedirectView(RedirectView):
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
agenda = get_object_or_404(Agenda, slug=kwargs['slug'])
|
|
return reverse('lingo-manager-agenda-detail', kwargs={'pk': agenda.pk})
|
|
|
|
|
|
agenda_detail_redirect = AgendaDetailRedirectView.as_view()
|
|
|
|
|
|
class AgendaExport(AgendaMixin, DetailView):
|
|
model = Agenda
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
response = HttpResponse(content_type='application/json')
|
|
today = datetime.date.today()
|
|
response['Content-Disposition'] = 'attachment; filename="export_pricing_agenda_{}_{}.json"'.format(
|
|
self.get_object().slug, today.strftime('%Y%m%d')
|
|
)
|
|
json.dump({'agendas': [self.get_object().export_json()]}, response, indent=2)
|
|
return response
|
|
|
|
|
|
agenda_export = AgendaExport.as_view()
|
|
|
|
|
|
class AgendaBookingCheckSettingsView(AgendaMixin, UpdateView):
|
|
template_name = 'lingo/pricing/manager_agenda_form.html'
|
|
model = Agenda
|
|
fields = ['check_type_group']
|
|
tab_anchor = 'check'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['form_url'] = reverse('lingo-manager-agenda-booking-check-settings', args=[self.agenda.pk])
|
|
context['title'] = _("Configure booking check options")
|
|
return context
|
|
|
|
|
|
agenda_booking_check_settings = AgendaBookingCheckSettingsView.as_view()
|
|
|
|
|
|
class AgendaInvoicingSettingsView(AgendaMixin, UpdateView):
|
|
template_name = 'lingo/pricing/manager_agenda_form.html'
|
|
model = Agenda
|
|
fields = ['regie']
|
|
tab_anchor = 'invoicing'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['form_url'] = reverse('lingo-manager-agenda-invoicing-settings', args=[self.agenda.pk])
|
|
context['title'] = _("Configure invoicing options")
|
|
return context
|
|
|
|
|
|
agenda_invoicing_settings = AgendaInvoicingSettingsView.as_view()
|
|
|
|
|
|
class AgendaPricingListView(ListView):
|
|
template_name = 'lingo/pricing/manager_agenda_pricing_list.html'
|
|
model = AgendaPricing
|
|
|
|
def get_queryset(self):
|
|
return (
|
|
AgendaPricing.objects.all()
|
|
.select_related('pricing')
|
|
.order_by('flat_fee_schedule', 'date_start', 'date_end')
|
|
)
|
|
|
|
|
|
agenda_pricing_list = AgendaPricingListView.as_view()
|
|
|
|
|
|
class AgendaPricingAddView(CreateView):
|
|
template_name = 'lingo/pricing/manager_agenda_pricing_form.html'
|
|
model = AgendaPricing
|
|
form_class = NewAgendaPricingForm
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-agenda-pricing-detail', args=[self.object.pk])
|
|
|
|
|
|
agenda_pricing_add = AgendaPricingAddView.as_view()
|
|
|
|
|
|
class AgendaPricingDetailView(DetailView):
|
|
model = AgendaPricing
|
|
template_name = 'lingo/pricing/manager_agenda_pricing_detail.html'
|
|
|
|
def get_queryset(self):
|
|
return AgendaPricing.objects.all().prefetch_related('pricing__criterias__category')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
form = PricingTestToolForm(
|
|
agenda_pricing=self.object, request=self.request, data=self.request.GET or None
|
|
)
|
|
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)
|
|
|
|
|
|
agenda_pricing_detail = AgendaPricingDetailView.as_view()
|
|
|
|
|
|
class AgendaPricingTestToolView(RedirectView):
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
return '%s?%s#open:debug' % (
|
|
reverse('lingo-manager-agenda-pricing-detail', args=[kwargs['pk']]),
|
|
self.request.GET.urlencode(),
|
|
)
|
|
|
|
|
|
agenda_pricing_test_tool = AgendaPricingTestToolView.as_view()
|
|
|
|
|
|
class AgendaPricingEditView(UpdateView):
|
|
template_name = 'lingo/pricing/manager_agenda_pricing_form.html'
|
|
model = AgendaPricing
|
|
form_class = AgendaPricingForm
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-agenda-pricing-detail', args=[self.object.pk])
|
|
|
|
|
|
agenda_pricing_edit = AgendaPricingEditView.as_view()
|
|
|
|
|
|
class AgendaPricingDeleteView(DeleteView):
|
|
template_name = 'lingo/manager_confirm_delete.html'
|
|
model = AgendaPricing
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-agenda-pricing-list')
|
|
|
|
|
|
agenda_pricing_delete = AgendaPricingDeleteView.as_view()
|
|
|
|
|
|
class AgendaPricingExport(DetailView):
|
|
model = AgendaPricing
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
response = HttpResponse(content_type='application/json')
|
|
today = datetime.date.today()
|
|
attachment = 'attachment; filename="export_pricing_{}_{}.json"'.format(
|
|
self.get_object().pricing.slug, today.strftime('%Y%m%d')
|
|
)
|
|
response['Content-Disposition'] = attachment
|
|
json.dump({'pricings': [self.get_object().export_json()]}, response, indent=2)
|
|
return response
|
|
|
|
|
|
agenda_pricing_export = AgendaPricingExport.as_view()
|
|
|
|
|
|
class AgendaPricingAgendaAddView(FormView):
|
|
template_name = 'lingo/pricing/manager_agenda_pricing_agenda_form.html'
|
|
model = AgendaPricing
|
|
form_class = AgendaPricingAgendaAddForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.object = get_object_or_404(AgendaPricing, pk=kwargs['pk'], subscription_required=True)
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['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):
|
|
self.object.agendas.add(form.cleaned_data['agenda'])
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return '%s#open:agendas' % reverse('lingo-manager-agenda-pricing-detail', args=[self.object.pk])
|
|
|
|
|
|
agenda_pricing_agenda_add = AgendaPricingAgendaAddView.as_view()
|
|
|
|
|
|
class AgendaPricingAgendaDeleteView(DeleteView):
|
|
template_name = 'lingo/manager_confirm_delete.html'
|
|
model = Agenda
|
|
pk_url_kwarg = 'agenda_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.agendas.all()
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
self.agenda_pricing.agendas.remove(self.object)
|
|
return HttpResponseRedirect(self.get_success_url())
|
|
|
|
def get_success_url(self):
|
|
return '%s#open:agendas' % reverse(
|
|
'lingo-manager-agenda-pricing-detail', args=[self.agenda_pricing.pk]
|
|
)
|
|
|
|
|
|
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'
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.object = get_object_or_404(AgendaPricing, pk=kwargs['pk'])
|
|
matrix_list = list(self.object.iter_pricing_matrix())
|
|
if not matrix_list:
|
|
raise Http404
|
|
self.matrix = None
|
|
if kwargs.get('slug'):
|
|
for matrix in matrix_list:
|
|
if matrix.criteria is None:
|
|
continue
|
|
if matrix.criteria.slug == kwargs['slug']:
|
|
self.matrix = matrix
|
|
break
|
|
else:
|
|
if matrix_list[0].criteria is None:
|
|
self.matrix = matrix_list[0]
|
|
if self.matrix is None:
|
|
raise Http404
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
kwargs['object'] = self.object
|
|
kwargs['matrix'] = self.matrix
|
|
return super().get_context_data(**kwargs)
|
|
|
|
def get_form(self):
|
|
count = len(self.matrix.rows)
|
|
PricingMatrixFormSet = forms.formset_factory(
|
|
PricingMatrixForm, min_num=count, max_num=count, extra=0, can_delete=False
|
|
)
|
|
kwargs = {
|
|
'initial': [
|
|
{'crit_%i' % i: cell.value for i, cell in enumerate(row.cells)} for row in self.matrix.rows
|
|
]
|
|
}
|
|
if self.request.method == 'POST':
|
|
kwargs.update(
|
|
{
|
|
'data': self.request.POST,
|
|
}
|
|
)
|
|
return PricingMatrixFormSet(form_kwargs={'matrix': self.matrix}, **kwargs)
|
|
|
|
def post(self, *args, **kwargs):
|
|
form = self.get_form()
|
|
if form.is_valid():
|
|
# build prixing_data for this matrix
|
|
matrix_pricing_data = defaultdict(dict)
|
|
for i, sub_data in enumerate(form.cleaned_data):
|
|
row = self.matrix.rows[i]
|
|
for j, cell in enumerate(row.cells):
|
|
value = sub_data['crit_%s' % j]
|
|
key = cell.criteria.identifier if cell.criteria else None
|
|
matrix_pricing_data[key][row.criteria.identifier] = float(value)
|
|
if self.matrix.criteria:
|
|
# full pricing model with 3 categories
|
|
self.object.pricing_data = self.object.pricing_data or {}
|
|
self.object.pricing_data[self.matrix.criteria.identifier] = matrix_pricing_data
|
|
elif list(matrix_pricing_data.keys()) == [None]:
|
|
# only one category
|
|
self.object.pricing_data = matrix_pricing_data[None]
|
|
else:
|
|
# 2 categories
|
|
self.object.pricing_data = matrix_pricing_data
|
|
self.object.save()
|
|
return self.form_valid(form)
|
|
else:
|
|
return self.form_invalid(form)
|
|
|
|
def get_success_url(self):
|
|
return '%s#open:matrix-%s' % (
|
|
reverse('lingo-manager-agenda-pricing-detail', args=[self.object.pk]),
|
|
self.kwargs.get('slug') or '',
|
|
)
|
|
|
|
|
|
agenda_pricing_matrix_edit = AgendaPricingMatrixEdit.as_view()
|
|
|
|
|
|
class CheckTypeListView(ListView):
|
|
template_name = 'lingo/pricing/manager_check_type_list.html'
|
|
model = CheckTypeGroup
|
|
|
|
def get_queryset(self):
|
|
return CheckTypeGroup.objects.prefetch_related('check_types')
|
|
|
|
|
|
check_type_list = CheckTypeListView.as_view()
|
|
|
|
|
|
class CheckTypeGroupAddView(CreateView):
|
|
template_name = 'lingo/pricing/manager_check_type_group_form.html'
|
|
model = CheckTypeGroup
|
|
fields = ['label']
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-check-type-list')
|
|
|
|
|
|
check_type_group_add = CheckTypeGroupAddView.as_view()
|
|
|
|
|
|
class CheckTypeGroupEditView(UpdateView):
|
|
template_name = 'lingo/pricing/manager_check_type_group_form.html'
|
|
model = CheckTypeGroup
|
|
fields = ['label', 'slug']
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-check-type-list')
|
|
|
|
|
|
check_type_group_edit = CheckTypeGroupEditView.as_view()
|
|
|
|
|
|
class CheckTypeGroupDeleteView(DeleteView):
|
|
template_name = 'lingo/manager_confirm_delete.html'
|
|
model = CheckTypeGroup
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-check-type-list')
|
|
|
|
|
|
check_type_group_delete = CheckTypeGroupDeleteView.as_view()
|
|
|
|
|
|
class CheckTypeGroupExport(DetailView):
|
|
model = CheckTypeGroup
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
response = HttpResponse(content_type='application/json')
|
|
today = datetime.date.today()
|
|
attachment = 'attachment; filename="export_check_type_group_{}_{}.json"'.format(
|
|
self.get_object().slug, today.strftime('%Y%m%d')
|
|
)
|
|
response['Content-Disposition'] = attachment
|
|
json.dump({'check_type_groups': [self.get_object().export_json()]}, response, indent=2)
|
|
return response
|
|
|
|
|
|
check_type_group_export = CheckTypeGroupExport.as_view()
|
|
|
|
|
|
class CheckTypeAddView(CreateView):
|
|
template_name = 'lingo/pricing/manager_check_type_form.html'
|
|
model = CheckType
|
|
form_class = NewCheckTypeForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.group_pk = kwargs.pop('group_pk')
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
if not kwargs.get('instance'):
|
|
kwargs['instance'] = self.model()
|
|
kwargs['instance'].group_id = self.group_pk
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-check-type-list')
|
|
|
|
|
|
check_type_add = CheckTypeAddView.as_view()
|
|
|
|
|
|
class CheckTypeEditView(UpdateView):
|
|
template_name = 'lingo/pricing/manager_check_type_form.html'
|
|
model = CheckType
|
|
form_class = CheckTypeForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.group_pk = kwargs.pop('group_pk')
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
return CheckType.objects.filter(group=self.group_pk)
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-check-type-list')
|
|
|
|
|
|
check_type_edit = CheckTypeEditView.as_view()
|
|
|
|
|
|
class CheckTypeDeleteView(DeleteView):
|
|
template_name = 'lingo/manager_confirm_delete.html'
|
|
model = CheckType
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.group_pk = kwargs.pop('group_pk')
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
return CheckType.objects.filter(group=self.group_pk)
|
|
|
|
def get_success_url(self):
|
|
return reverse('lingo-manager-check-type-list')
|
|
|
|
|
|
check_type_delete = CheckTypeDeleteView.as_view()
|