Compare commits

...

6 Commits

Author SHA1 Message Date
Lauréline Guérin fc8872e559
misc: remove import/export commands, simplify code (#78125)
gitea/lingo/pipeline/head This commit looks good Details
2023-06-02 15:44:00 +02:00
Lauréline Guérin 2b3f25985b
invoicing: add a global export/import for invoicing config (#78125) 2023-06-02 14:37:27 +02:00
Lauréline Guérin 1813a9c415
misc: rename AgendaImportError to be more generic (#78125) 2023-06-02 11:28:51 +02:00
Lauréline Guérin 0eadd96ae8
pricing: add the edition of regie slug (#78125) 2023-06-02 11:28:41 +02:00
Lauréline Guérin 05c08d9098
invoicing: reorganize regie templates (#78125) 2023-06-02 11:28:30 +02:00
Lauréline Guérin dc8b89112a
invoicing: split views (#78125) 2023-06-02 11:28:17 +02:00
29 changed files with 1368 additions and 1297 deletions

View File

@ -21,7 +21,7 @@ from django.db import models
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from lingo.utils.misc import AgendaImportError, clean_import_data, generate_slug
from lingo.utils.misc import LingoImportError, clean_import_data, generate_slug
class Agenda(models.Model):
@ -66,17 +66,17 @@ class Agenda(models.Model):
}
@classmethod
def import_json(cls, data, overwrite=False):
def import_json(cls, data):
data = copy.deepcopy(data)
try:
agenda = Agenda.objects.get(slug=data['slug'])
except Agenda.DoesNotExist:
raise AgendaImportError(_('Missing "%s" agenda') % data['slug'])
raise LingoImportError(_('Missing "%s" agenda') % data['slug'])
if data.get('check_type_group'):
try:
data['check_type_group'] = CheckTypeGroup.objects.get(slug=data['check_type_group'])
except CheckTypeGroup.DoesNotExist:
raise AgendaImportError(_('Missing "%s" check type group') % data['check_type_group'])
raise LingoImportError(_('Missing "%s" check type group') % data['check_type_group'])
agenda.check_type_group = data.get('check_type_group')
agenda.save()
@ -111,14 +111,11 @@ class CheckTypeGroup(models.Model):
return slugify(self.label)
@classmethod
def import_json(cls, data, overwrite=False):
def import_json(cls, data):
check_types = data.pop('check_types', [])
data = clean_import_data(cls, data)
group, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
if overwrite:
CheckType.objects.filter(group=group).delete()
for check_type in check_types:
check_type['group'] = group
CheckType.import_json(check_type)

View File

@ -20,7 +20,29 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from gadjo.forms.widgets import MultiSelectWidget
from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Invoice, InvoiceLine
from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Invoice, InvoiceLine, Regie
class ExportForm(forms.Form):
regies = forms.BooleanField(label=_('Regies'), required=False, initial=True)
class ImportForm(forms.Form):
config_json = forms.FileField(label=_('Export File'))
class RegieForm(forms.ModelForm):
class Meta:
model = Regie
fields = ['label', 'slug', 'description', 'cashier_role', 'counter_name', 'number_format']
def clean_slug(self):
slug = self.cleaned_data['slug']
if Regie.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError(_('Another regie exists with the same identifier.'))
return slug
class CampaignForm(forms.ModelForm):

View File

@ -0,0 +1,22 @@
{% extends "lingo/invoicing/manager_home.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-config-export' %}">{% trans 'Export' %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Export" %}</h2>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button class="submit-button">{% trans "Export" %}</button>
<a class="cancel" href="{% url 'lingo-manager-invoicing-home' %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -1,13 +1,13 @@
{% extends "lingo/invoicing/manager_regie_list.html" %}
{% extends "lingo/invoicing/manager_home.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-regie-import' %}">{% trans 'Import' %}</a>
<a href="{% url 'lingo-manager-invoicing-config-import' %}">{% trans 'Import' %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Import Regies" %}</h2>
<h2>{% trans "Import" %}</h2>
{% endblock %}
{% block content %}
@ -16,7 +16,7 @@
{{ form.as_p }}
<div class="buttons">
<button class="submit-button">{% trans "Import" %}</button>
<a class="cancel" href="{% url 'lingo-manager-invoicing-regie-list' %}">{% trans 'Cancel' %}</a>
<a class="cancel" href="{% url 'lingo-manager-invoicing-home' %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -8,6 +8,13 @@
{% block appbar %}
<h2>{% trans 'Invoicing' %}</h2>
<span class="actions">
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
<li><a rel="popup" href="{% url 'lingo-manager-invoicing-config-import' %}">{% trans 'Import' %}</a></li>
<li><a rel="popup" href="{% url 'lingo-manager-invoicing-config-export' %}" data-autoclose-dialog="true">{% trans 'Export' %}</a></li>
</ul>
</span>
{% endblock %}
{% block content %}

View File

@ -1,12 +0,0 @@
{% extends "lingo/invoicing/manager_home.html" %}
{% load i18n %}
{% block appbar %}
<h2>{% if regie %}{% trans 'Regie' %} - {{regie}} {% else %}{% trans 'Regies' %}{% endif %}</h2>
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-regie-list' %}">{% trans "Regies" %}</a>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "lingo/invoicing/manager_regie_common.html" %}
{% extends "lingo/invoicing/manager_regie_list.html" %}
{% load i18n gadjo %}
{% block breadcrumb %}
@ -7,12 +7,16 @@
{% endblock %}
{% block appbar %}
{{ block.super }}
<h2>{% trans 'Regie' %} - {{ regie }}</h2>
<span class="actions">
{% if not has_related_objects %}
<a href="{% url 'lingo-manager-invoicing-regie-delete' pk=regie.pk %}" rel="popup">{% trans "Delete" %}</a>
{% endif %}
<a href="{% url 'lingo-manager-invoicing-regie-edit' pk=regie.pk %}" rel="popup">{% trans "Edit" %}</a>
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
<li><a href="{% url 'lingo-manager-invoicing-regie-edit' pk=regie.pk %}" rel="popup">{% trans "Edit" %}</a></li>
<li><a href="{% url 'lingo-manager-invoicing-regie-export' pk=regie.pk %}">{% trans 'Export' %}</a></li>
{% if not has_related_objects %}
<li><a href="{% url 'lingo-manager-invoicing-regie-delete' pk=regie.pk %}" rel="popup">{% trans "Delete" %}</a></li>
{% endif %}
</ul>
<a href="{% url 'lingo-manager-invoicing-non-invoiced-line-list' regie_pk=regie.pk %}">{% trans 'Non invoiced lines' %}</a>
<a href="{% url 'lingo-manager-invoicing-regie-invoice-list' regie_pk=regie.pk %}">{% trans 'Invoices' %}</a>
</span>

View File

@ -1,4 +1,4 @@
{% extends "lingo/invoicing/manager_regie_common.html" %}
{% extends "lingo/invoicing/manager_regie_list.html" %}
{% load i18n gadjo %}
{% block breadcrumb %}
@ -13,7 +13,7 @@
{% block appbar %}
{% if regie.pk %}
<h2>{% trans "Edit regie" %} - {{regie}}</h2>
<h2>{% trans "Edit regie" %} - {{ regie }}</h2>
{% else %}
<h2>{% trans "New regie" %}</h2>
{% endif %}
@ -25,7 +25,11 @@
{{ form|with_template }}
<div class="buttons">
<button>{% trans "Submit" %}</button>
<a class="cancel" href="{{cancel_url}}">{% trans 'Cancel' %}</a>
{% if object.pk %}
<a class="cancel" href="{% url 'lingo-manager-invoicing-regie-detail' regie.pk %}">{% trans 'Cancel' %}</a>
{% else %}
<a class="cancel" href="{% url 'lingo-manager-invoicing-regie-list' %}">{% trans 'Cancel' %}</a>
{% endif %}
</div>
</form>
{% endblock %}

View File

@ -1,18 +1,14 @@
{% extends "lingo/invoicing/manager_regie_common.html" %}
{% extends "lingo/invoicing/manager_home.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-invoicing-regie-list' %}">{% trans "Regies" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Regies' %}</h2>
<span class="actions">
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
<li>
<a href="{% url 'lingo-manager-invoicing-regie-import' %}">{% trans 'Import' %}</a>
</li>
<li>
<a href="{% url 'lingo-manager-invoicing-regie-export' %}">{% trans 'Export' %}</a>
</li>
</ul>
<a rel="popup" href="{% url 'lingo-manager-invoicing-regie-add' %}">{% trans 'New regie' %}</a>
</span>
{% endblock %}

View File

@ -16,126 +16,134 @@
from django.urls import path
from . import views
from .views import campaign as campaign_views
from .views import home as home_views
from .views import pool as pool_views
from .views import regie as regie_views
urlpatterns = [
path('', views.home, name='lingo-manager-invoicing-home'),
path('regies/', views.regies_list, name='lingo-manager-invoicing-regie-list'),
path('', home_views.home, name='lingo-manager-invoicing-home'),
path('import/', home_views.config_import, name='lingo-manager-invoicing-config-import'),
path('export/', home_views.config_export, name='lingo-manager-invoicing-config-export'),
path('regies/', regie_views.regies_list, name='lingo-manager-invoicing-regie-list'),
path(
'regie/add/',
views.regie_add,
regie_views.regie_add,
name='lingo-manager-invoicing-regie-add',
),
path(
'regie/<int:pk>/',
views.regie_detail,
regie_views.regie_detail,
name='lingo-manager-invoicing-regie-detail',
),
path(
'regie/<int:pk>/edit/',
views.regie_edit,
regie_views.regie_edit,
name='lingo-manager-invoicing-regie-edit',
),
path(
'regie/<int:pk>/delete/',
views.regie_delete,
regie_views.regie_delete,
name='lingo-manager-invoicing-regie-delete',
),
path('regies/import/', views.regies_import, name='lingo-manager-invoicing-regie-import'),
path('regies/export/', views.regies_export, name='lingo-manager-invoicing-regie-export'),
path(
'regie/<int:pk>/export/',
regie_views.regie_export,
name='lingo-manager-invoicing-regie-export',
),
path(
'regie/<int:regie_pk>/campaign/add/',
views.campaign_add,
campaign_views.campaign_add,
name='lingo-manager-invoicing-campaign-add',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/',
views.campaign_detail,
campaign_views.campaign_detail,
name='lingo-manager-invoicing-campaign-detail',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/edit/',
views.campaign_edit,
campaign_views.campaign_edit,
name='lingo-manager-invoicing-campaign-edit',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/dates/edit/',
views.campaign_dates_edit,
campaign_views.campaign_dates_edit,
name='lingo-manager-invoicing-campaign-dates-edit',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/delete/',
views.campaign_delete,
campaign_views.campaign_delete,
name='lingo-manager-invoicing-campaign-delete',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/unlock-check/',
views.campaign_unlock_check,
campaign_views.campaign_unlock_check,
name='lingo-manager-invoicing-campaign-unlock-check',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/finalize/',
views.campaign_finalize,
campaign_views.campaign_finalize,
name='lingo-manager-invoicing-campaign-finalize',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/pool/add/',
views.pool_add,
pool_views.pool_add,
name='lingo-manager-invoicing-pool-add',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/pool/<int:pool_pk>/',
views.pool_detail,
pool_views.pool_detail,
name='lingo-manager-invoicing-pool-detail',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/pool/<int:pool_pk>/journal/',
views.pool_journal,
pool_views.pool_journal,
name='lingo-manager-invoicing-pool-journal',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/pool/<int:pool_pk>/promote/',
views.pool_promote,
pool_views.pool_promote,
name='lingo-manager-invoicing-pool-promote',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/pool/<int:pool_pk>/delete/',
views.pool_delete,
pool_views.pool_delete,
name='lingo-manager-invoicing-pool-delete',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/pool/<int:pool_pk>/invoice/<int:invoice_pk>/pdf/',
views.invoice_pdf,
pool_views.invoice_pdf,
name='lingo-manager-invoicing-invoice-pdf',
),
path(
'ajax/regie/<int:regie_pk>/campaign/<int:pk>/pool/<int:pool_pk>/invoice/<int:invoice_pk>/lines/',
views.invoice_line_list,
pool_views.invoice_line_list,
name='lingo-manager-invoicing-invoice-line-list',
),
path(
'ajax/regie/<int:regie_pk>/campaign/<int:pk>/pool/<int:pool_pk>/line/<int:line_pk>/<slug:status>/',
views.line_set_error_status,
pool_views.line_set_error_status,
name='lingo-manager-invoicing-line-set-error-status',
),
path(
'regie/<int:regie_pk>/non-invoiced-lines/',
views.non_invoiced_line_list,
regie_views.non_invoiced_line_list,
name='lingo-manager-invoicing-non-invoiced-line-list',
),
path(
'regie/<int:regie_pk>/invoices/',
views.regie_invoice_list,
regie_views.regie_invoice_list,
name='lingo-manager-invoicing-regie-invoice-list',
),
path(
'regie/<int:regie_pk>/invoice/<int:invoice_pk>/pdf/',
views.regie_invoice_pdf,
regie_views.regie_invoice_pdf,
name='lingo-manager-invoicing-regie-invoice-pdf',
),
path(
'ajax/regie/<int:regie_pk>/invoice/<int:invoice_pk>/lines/',
views.regie_invoice_line_list,
regie_views.regie_invoice_line_list,
name='lingo-manager-invoicing-regie-invoice-line-list',
),
]

View File

@ -17,12 +17,13 @@
import collections
import datetime
from django.db import transaction
from django.test.client import RequestFactory
from django.utils.translation import gettext_lazy as _
from lingo.agendas.chrono import get_check_status, get_subscriptions
from lingo.agendas.models import Agenda
from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, InjectedLine
from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, InjectedLine, Regie
from lingo.pricing.models import AgendaPricing, AgendaPricingNotFound, PricingError
@ -310,3 +311,34 @@ def generate_invoices_from_lines(all_lines, pool):
invoices.append(invoice)
return invoices
def export_site(
regies=True,
):
'''Dump site objects to JSON-dumpable dictionnary'''
data = {}
if regies:
data['regies'] = [x.export_json() for x in Regie.objects.all()]
return data
def import_site(data):
results = {
key: collections.defaultdict(list)
for key in [
'regies',
]
}
with transaction.atomic():
for cls, key in ((Regie, 'regies'),):
objs = data.get(key, [])
for obj in objs:
created, obj = cls.import_json(obj)
results[key]['all'].append(obj)
if created:
results[key]['created'].append(obj)
else:
results[key]['updated'].append(obj)
return results

View File

@ -1,929 +0,0 @@
# 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 collections
import datetime
import json
from django.contrib import messages
from django.db import transaction
from django.db.models import CharField, Count, IntegerField, JSONField, OuterRef, Subquery, Value
from django.db.models.functions import Coalesce
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from django.views.generic import (
CreateView,
DeleteView,
DetailView,
FormView,
ListView,
TemplateView,
UpdateView,
)
from weasyprint import HTML
from lingo.agendas.chrono import ChronoError, mark_events_invoiced, unlock_events_check
from lingo.agendas.models import Agenda
from lingo.invoicing.forms import (
CampaignDatesForm,
CampaignForm,
DraftInvoiceFilterSet,
DraftInvoiceLineFilterSet,
InvoiceFilterSet,
InvoiceLineFilterSet,
RegieInvoiceFilterSet,
)
from lingo.invoicing.models import (
Campaign,
Counter,
DraftInvoice,
DraftInvoiceLine,
InjectedLine,
Invoice,
InvoiceLine,
InvoicePayment,
Pool,
Regie,
RegieImportError,
)
from lingo.pricing.forms import ImportForm
def is_ajax(request):
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
def import_regies(data):
results = collections.defaultdict(list)
with transaction.atomic():
regies = data.get('regies', [])
for regie in regies:
created, regie_obj = Regie.import_json(regie)
if created:
results['created'].append(regie_obj)
else:
results['updated'].append(regie_obj)
return results
class HomeView(TemplateView):
template_name = 'lingo/invoicing/manager_home.html'
home = HomeView.as_view()
class RegiesListView(ListView):
template_name = 'lingo/invoicing/manager_regie_list.html'
model = Regie
regies_list = RegiesListView.as_view()
class RegieAddView(CreateView):
template_name = 'lingo/invoicing/manager_regie_form.html'
model = Regie
fields = ['label', 'description', 'cashier_role']
def get_success_url(self):
return reverse('lingo-manager-invoicing-regie-detail', args=[self.object.pk])
regie_add = RegieAddView.as_view()
class RegieDetailView(DetailView):
template_name = 'lingo/invoicing/manager_regie_detail.html'
model = Regie
def get_context_data(self, **kwargs):
kwargs['regie'] = self.object
kwargs['agendas'] = Agenda.objects.filter(regie=self.object)
kwargs['campaigns'] = self.object.campaign_set.all().order_by('-date_start')
has_related_objects = False
if kwargs['campaigns']:
has_related_objects = True
elif self.object.injectedline_set.exists():
has_related_objects = True
kwargs['has_related_objects'] = has_related_objects
return super().get_context_data(**kwargs)
regie_detail = RegieDetailView.as_view()
class RegieEditView(UpdateView):
template_name = 'lingo/invoicing/manager_regie_form.html'
model = Regie
fields = ['label', 'description', 'cashier_role', 'counter_name', 'number_format']
def get_success_url(self):
return reverse('lingo-manager-invoicing-regie-detail', args=[self.object.pk])
regie_edit = RegieEditView.as_view()
class RegieDeleteView(DeleteView):
template_name = 'lingo/manager_confirm_delete.html'
model = Regie
def get_queryset(self):
return super().get_queryset().filter(campaign__isnull=True, injectedline__isnull=True)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
Counter.objects.filter(regie=self.object).delete()
return super().delete(request, *args, **kwargs)
def get_success_url(self):
return reverse('lingo-manager-invoicing-regie-list')
regie_delete = RegieDeleteView.as_view()
class RegiesExportView(ListView):
model = Regie
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
attachment = 'attachment; filename="export_regies_{}.json"'.format(today.strftime('%Y%m%d'))
response['Content-Disposition'] = attachment
json.dump({'regies': [regie.export_json() for regie in self.get_queryset()]}, response, indent=2)
return response
regies_export = RegiesExportView.as_view()
class RegiesImportView(FormView):
form_class = ImportForm
template_name = 'lingo/invoicing/manager_import.html'
success_url = reverse_lazy('lingo-manager-invoicing-regie-list')
def form_valid(self, form):
try:
config_json = json.loads(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_regies(config_json)
except RegieImportError as exc:
form.add_error('config_json', '%s' % exc)
return self.form_invalid(form)
import_messages = {
'create': lambda x: ngettext(
'A regie was created.',
'%(count)d regies were created.',
x,
),
'update': lambda x: ngettext(
'A regie was updated.',
'%(count)d regie were updated.',
x,
),
}
create_message = _('No regie created.')
update_message = _('No regie updated.')
created = len(results.get('created', []))
updated = len(results.get('updated', []))
if created:
create_message = import_messages.get('create')(created) % {'count': created}
if updated:
update_message = import_messages.get('update')(updated) % {'count': updated}
message = "%s %s" % (create_message, update_message)
messages.info(self.request, message)
return super().form_valid(form)
regies_import = RegiesImportView.as_view()
class CampaignAddView(CreateView):
template_name = 'lingo/invoicing/manager_campaign_form.html'
model = Campaign
form_class = CampaignForm
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
return super().get_context_data(**kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['instance'] = Campaign(regie=self.regie)
return kwargs
def get_success_url(self):
return reverse('lingo-manager-invoicing-campaign-detail', args=[self.regie.pk, self.object.pk])
campaign_add = CampaignAddView.as_view()
class CampaignDetailView(DetailView):
template_name = 'lingo/invoicing/manager_campaign_detail.html'
model = Campaign
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return super().get_queryset().filter(regie=self.regie)
def get_context_data(self, **kwargs):
draft_lines = DraftInvoiceLine.objects.filter(pool=OuterRef('pk')).order_by().values('pool')
count_draft_error = draft_lines.filter(status='error').annotate(count=Count('pool')).values('count')
count_draft_warning = (
draft_lines.filter(status='warning').annotate(count=Count('pool')).values('count')
)
count_draft_success = (
draft_lines.filter(status='success').annotate(count=Count('pool')).values('count')
)
lines = InvoiceLine.objects.filter(pool=OuterRef('pk')).order_by().values('pool')
count_error = (
lines.filter(status='error', error_status='').annotate(count=Count('pool')).values('count')
)
count_warning = lines.filter(status='warning').annotate(count=Count('pool')).values('count')
count_success = lines.filter(status='success').annotate(count=Count('pool')).values('count')
kwargs['regie'] = self.regie
kwargs['pools'] = self.object.pool_set.annotate(
draft_error_count=Coalesce(Subquery(count_draft_error, output_field=IntegerField()), Value(0)),
draft_warning_count=Coalesce(
Subquery(count_draft_warning, output_field=IntegerField()), Value(0)
),
draft_success_count=Coalesce(
Subquery(count_draft_success, output_field=IntegerField()), Value(0)
),
error_count=Coalesce(Subquery(count_error, output_field=IntegerField()), Value(0)),
warning_count=Coalesce(Subquery(count_warning, output_field=IntegerField()), Value(0)),
success_count=Coalesce(Subquery(count_success, output_field=IntegerField()), Value(0)),
).order_by('-created_at')
kwargs['has_running_pool'] = any(p.status in ['registered', 'running'] for p in kwargs['pools'])
kwargs['has_real_pool'] = any(not p.draft for p in kwargs['pools'])
kwargs['has_real_completed_pool'] = any(
not p.draft and p.status == 'completed' for p in kwargs['pools']
)
if self.object.invalid:
messages.warning(self.request, _('The last pool is invalid, please start a new pool.'))
return super().get_context_data(**kwargs)
campaign_detail = CampaignDetailView.as_view()
class CampaignEditView(UpdateView):
template_name = 'lingo/invoicing/manager_campaign_form.html'
model = Campaign
form_class = CampaignForm
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return (
super()
.get_queryset()
.filter(regie=self.regie, finalized=False)
.exclude(pool__draft=False)
.exclude(pool__status__in=['registered', 'running'])
)
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
return super().get_context_data(**kwargs)
def get_success_url(self):
return reverse('lingo-manager-invoicing-campaign-detail', args=[self.regie.pk, self.object.pk])
campaign_edit = CampaignEditView.as_view()
class CampaignDatesEditView(UpdateView):
template_name = 'lingo/invoicing/manager_campaign_dates_form.html'
model = Campaign
form_class = CampaignDatesForm
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return (
super()
.get_queryset()
.filter(regie=self.regie, finalized=False)
.exclude(pool__status__in=['registered', 'running'])
)
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
return super().get_context_data(**kwargs)
def get_success_url(self):
return '%s#open:dates' % reverse(
'lingo-manager-invoicing-campaign-detail', args=[self.regie.pk, self.object.pk]
)
campaign_dates_edit = CampaignDatesEditView.as_view()
class CampaignDeleteView(DeleteView):
template_name = 'lingo/manager_confirm_delete.html'
model = Campaign
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return (
super()
.get_queryset()
.filter(regie=self.regie, finalized=False)
.exclude(pool__draft=False)
.exclude(pool__status__in=['registered', 'running'])
)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
DraftInvoiceLine.objects.filter(pool__campaign=self.object).delete()
DraftInvoice.objects.filter(pool__campaign=self.object).delete()
Pool.objects.filter(campaign=self.object).delete()
return super().delete(request, *args, **kwargs)
def get_success_url(self):
return '%s#open:campaigns' % reverse('lingo-manager-invoicing-regie-detail', args=[self.regie.pk])
campaign_delete = CampaignDeleteView.as_view()
class CampaignUnlockCheckView(FormView):
template_name = 'lingo/invoicing/manager_campaign_unlock_check.html'
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.object = get_object_or_404(
Campaign.objects.filter(regie=self.regie, invalid=False, finalized=False)
.exclude(pool__draft=False)
.exclude(pool__status__in=['registered', 'running']),
pk=kwargs['pk'],
)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['form'] = None
kwargs['regie'] = self.regie
kwargs['object'] = self.object
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
self.object.mark_as_invalid()
agendas = [a.slug for a in self.object.agendas.all()]
if agendas:
try:
unlock_events_check(
agenda_slugs=agendas,
date_start=self.object.date_start,
date_end=self.object.date_end,
)
except ChronoError as e:
messages.error(self.request, _('Fail to unlock events check: %s') % e)
return redirect(
'%s#open:pools'
% reverse('lingo-manager-invoicing-campaign-detail', args=[self.regie.pk, self.object.pk])
)
campaign_unlock_check = CampaignUnlockCheckView.as_view()
class CampaignFinalizeView(FormView):
template_name = 'lingo/invoicing/manager_campaign_finalize.html'
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.object = get_object_or_404(
Campaign.objects.filter(regie=self.regie, invalid=False, finalized=False).filter(
pk__in=Pool.objects.filter(draft=False, status='completed').values('campaign')
),
pk=kwargs['pk'],
)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['form'] = None
kwargs['regie'] = self.regie
kwargs['object'] = self.object
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
try:
agendas = [a.slug for a in self.object.agendas.all()]
if agendas:
try:
mark_events_invoiced(
agenda_slugs=agendas,
date_start=self.object.date_start,
date_end=self.object.date_end,
)
except ChronoError as e:
messages.error(self.request, _('Fail to mark events as invoiced: %s') % e)
raise
except ChronoError:
pass
else:
self.object.mark_as_finalized()
return redirect(
'%s#open:pools'
% reverse('lingo-manager-invoicing-campaign-detail', args=[self.regie.pk, self.object.pk])
)
campaign_finalize = CampaignFinalizeView.as_view()
class PoolDetailView(ListView):
template_name = 'lingo/invoicing/manager_pool_detail.html'
paginate_by = 100
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.campaign = get_object_or_404(Campaign, pk=kwargs['pk'], regie=self.regie)
self.object = get_object_or_404(Pool, pk=kwargs['pool_pk'], campaign=self.campaign)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
invoice_model = Invoice
filter_model = InvoiceFilterSet
if self.object.draft:
invoice_model = DraftInvoice
filter_model = DraftInvoiceFilterSet
data = self.request.GET or None
self.filterset = filter_model(
data=data,
queryset=invoice_model.objects.filter(pool=self.object).order_by('created_at'),
pool=self.object,
)
return self.filterset.qs
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['object'] = self.campaign
kwargs['pool'] = self.object
kwargs['filterset'] = self.filterset
line_model = InvoiceLine
line_values = ['status', 'error_status']
if self.object.draft:
line_model = DraftInvoiceLine
line_values = ['status']
all_lines = line_model.objects.filter(pool=self.object).values(*line_values)
self.object.error_count = len(
[line for line in all_lines if line['status'] == 'error' and not line.get('error_status')]
)
self.object.warning_count = len([line for line in all_lines if line['status'] == 'warning'])
self.object.success_count = len([line for line in all_lines if line['status'] == 'success'])
kwargs['has_running_pool'] = any(
p.status in ['registered', 'running'] for p in self.campaign.pool_set.all()
)
return super().get_context_data(**kwargs)
pool_detail = PoolDetailView.as_view()
class PoolJournalView(ListView):
template_name = 'lingo/invoicing/manager_pool_journal.html'
paginate_by = 100
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.campaign = get_object_or_404(Campaign, pk=kwargs['pk'], regie=self.regie)
self.object = get_object_or_404(Pool, pk=kwargs['pool_pk'], campaign=self.campaign)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
line_model = InvoiceLine
filter_model = InvoiceLineFilterSet
if self.object.draft:
line_model = DraftInvoiceLine
filter_model = DraftInvoiceLineFilterSet
all_lines = line_model.objects.filter(pool=self.object).order_by('pk').select_related('invoice')
self.object.error_count = len(
[line for line in all_lines if line.status == 'error' and not getattr(line, 'error_status', '')]
)
self.object.warning_count = len([line for line in all_lines if line.status == 'warning'])
self.object.success_count = len([line for line in all_lines if line.status == 'success'])
data = self.request.GET or None
self.filterset = filter_model(data=data, queryset=all_lines, pool=self.object)
return self.filterset.qs if data and [v for v in data.values() if v] else all_lines
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['object'] = self.campaign
kwargs['pool'] = self.object
kwargs['filterset'] = self.filterset
return super().get_context_data(**kwargs)
pool_journal = PoolJournalView.as_view()
class PoolAddView(FormView):
template_name = 'lingo/invoicing/manager_pool_add.html'
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.object = get_object_or_404(
Campaign.objects.filter(regie=self.regie, finalized=False)
.exclude(pool__draft=False)
.exclude(pool__status__in=['registered', 'running']),
pk=kwargs['pk'],
)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['form'] = None
kwargs['regie'] = self.regie
kwargs['object'] = self.object
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
self.object.mark_as_valid()
self.object.generate()
return redirect(
'%s#open:pools'
% reverse('lingo-manager-invoicing-campaign-detail', args=[self.regie.pk, self.object.pk])
)
pool_add = PoolAddView.as_view()
class PoolPromoteView(FormView):
template_name = 'lingo/invoicing/manager_pool_promote.html'
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.object = get_object_or_404(
Pool,
campaign__id=kwargs['pk'],
campaign__regie=self.regie,
campaign__invalid=False,
campaign__finalized=False,
pk=kwargs['pool_pk'],
draft=True,
status='completed',
)
if not self.object.is_last:
raise Http404
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['form'] = None
kwargs['regie'] = self.regie
kwargs['object'] = self.object.campaign
kwargs['pool'] = self.object
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
self.object.promote()
return redirect(
'%s#open:pools'
% reverse(
'lingo-manager-invoicing-campaign-detail', args=[self.regie.pk, self.object.campaign.pk]
)
)
pool_promote = PoolPromoteView.as_view()
class PoolDeleteView(DeleteView):
template_name = 'lingo/manager_confirm_delete.html'
model = Pool
pk_url_kwarg = 'pool_pk'
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.campaign = get_object_or_404(
Campaign.objects.filter(regie=self.regie, finalized=False).exclude(
pool__status__in=['registered', 'running']
),
pk=kwargs['pk'],
)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.campaign.pool_set.all()
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.is_last and self.object.draft:
self.campaign.mark_as_invalid()
invoice_model = Invoice
line_model = InvoiceLine
if self.object.draft:
invoice_model = DraftInvoice
line_model = DraftInvoiceLine
line_model.objects.filter(pool=self.object).delete()
invoice_model.objects.filter(pool=self.object).delete()
return super().delete(request, *args, **kwargs)
def get_success_url(self):
return '%s#open:pools' % reverse(
'lingo-manager-invoicing-campaign-detail', args=[self.regie.pk, self.campaign.pk]
)
pool_delete = PoolDeleteView.as_view()
class PDFMixin:
def get(self, request, *args, **kwargs):
self.object = self.get_object()
result = self.object.html()
if 'html' in request.GET:
return HttpResponse(result)
html = HTML(string=result)
pdf = html.write_pdf()
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % self.object.formatted_number
return response
class InvoicePDFView(PDFMixin, DetailView):
pk_url_kwarg = 'invoice_pk'
model = Invoice
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.pool = get_object_or_404(
Pool, pk=kwargs['pool_pk'], campaign=kwargs['pk'], campaign__regie=self.regie
)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
invoice_model = Invoice
if self.pool.draft:
invoice_model = DraftInvoice
return invoice_model.objects.filter(pool=self.pool)
invoice_pdf = InvoicePDFView.as_view()
class InvoiceLineListView(ListView):
template_name = 'lingo/invoicing/manager_invoice_lines.html'
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.pool = get_object_or_404(
Pool, pk=kwargs['pool_pk'], campaign_id=kwargs['pk'], campaign__regie=self.regie
)
invoice_model = Invoice
if self.pool.draft:
invoice_model = DraftInvoice
self.invoice = get_object_or_404(invoice_model, pk=kwargs['invoice_pk'], pool=self.pool)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.invoice.lines.all().order_by('user_external_id', 'event_date', 'pk')
def get_context_data(self, **kwargs):
kwargs['regie'] = self.pool.campaign.regie
kwargs['object'] = self.pool.campaign
kwargs['pool'] = self.pool
if not self.pool.draft:
kwargs['invoice'] = self.invoice
kwargs['invoice_payments'] = (
InvoicePayment.objects.filter(invoice=self.invoice)
.select_related('payment')
.order_by('created_at')
)
return super().get_context_data(**kwargs)
invoice_line_list = InvoiceLineListView.as_view()
class LineSetErrorStatusView(DetailView):
model = InvoiceLine
pk_url_kwarg = 'line_pk'
template_name = 'lingo/invoicing/manager_line_detail_fragment.html'
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
status='error',
pool=self.kwargs['pool_pk'],
pool__campaign=self.kwargs['pk'],
pool__campaign__regie=self.regie,
)
)
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['object'] = self.object.pool.campaign
kwargs['pool'] = self.object.pool
kwargs['line'] = self.object
return super().get_context_data(**kwargs)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
error_status = kwargs['status']
if error_status == 'reset':
self.object.error_status = ''
elif error_status == 'ignore':
self.object.error_status = 'ignored'
elif error_status == 'fix':
self.object.error_status = 'fixed'
else:
raise Http404
self.object.save()
if is_ajax(self.request):
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
return redirect(
reverse(
'lingo-manager-invoicing-pool-journal', args=[self.regie.pk, kwargs['pk'], kwargs['pool_pk']]
)
)
line_set_error_status = LineSetErrorStatusView.as_view()
class NonInvoicedLineListView(ListView):
template_name = 'lingo/invoicing/manager_non_invoiced_line_list.html'
paginate_by = 100
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
fields = [
'pk',
'event_date',
'slug',
'label',
'quantity',
'unit_amount',
'total_amount',
'user_external_id',
'payer_external_id',
'payer_first_name',
'payer_last_name',
'user_first_name',
'user_last_name',
'event',
'pricing_data',
'status',
'pool_id',
]
qs1 = InvoiceLine.objects.filter(
status='error', error_status='', pool__campaign__regie=self.regie
).values(*fields)
qs2 = (
InjectedLine.objects.filter(invoiceline__isnull=True, regie=self.regie)
.annotate(
user_first_name=Value('', output_field=CharField()),
user_last_name=Value('', output_field=CharField()),
event=Value({}, output_field=JSONField()),
pricing_data=Value({}, output_field=JSONField()),
status=Value('injected', output_field=CharField()),
pool_id=Value(0, output_field=IntegerField()),
)
.values(*fields)
)
qs = qs1.union(qs2).order_by('event_date', 'user_external_id', 'label', 'pk')
return qs
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
context = super().get_context_data(**kwargs)
pools = Pool.objects.filter(draft=False).in_bulk()
for line in context['object_list']:
if line['status'] == 'error':
line['user_name'] = InvoiceLine(
user_first_name=line['user_first_name'], user_last_name=line['user_last_name']
).user_name
line['payer_name'] = InvoiceLine(
payer_first_name=line['payer_first_name'], payer_last_name=line['payer_last_name']
).payer_name
line['error_display'] = InvoiceLine(
status=line['status'], pricing_data=line['pricing_data']