lingo/lingo/invoicing/views/regie.py

759 lines
25 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 collections
import csv
import datetime
import json
from django.db import transaction
from django.db.models import CharField, IntegerField, JSONField, Prefetch, Q, Value
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import yesno
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from lingo.agendas.models import Agenda
from lingo.export_import.views import WithApplicationsMixin
from lingo.invoicing.forms import (
PaymentTypeForm,
RegieCreditFilterSet,
RegieForm,
RegieInvoiceFilterSet,
RegiePaymentFilterSet,
RegiePublishingForm,
RegieRefundFilterSet,
)
from lingo.invoicing.models import (
PAYMENT_INFO,
Counter,
Credit,
InjectedLine,
Invoice,
InvoiceLine,
InvoiceLinePayment,
JournalLine,
Payment,
PaymentType,
Pool,
Refund,
Regie,
)
from lingo.invoicing.views.utils import PDFMixin
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 RegiesListView(WithApplicationsMixin, ListView):
template_name = 'lingo/invoicing/manager_regie_list.html'
model = Regie
def dispatch(self, request, *args, **kwargs):
self.with_applications_dispatch(request)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.with_applications_queryset()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return self.with_applications_context_data(context)
regies_list = RegiesListView.as_view()
class RegieAddView(CreateView):
template_name = 'lingo/invoicing/manager_regie_form.html'
model = Regie
fields = ['label', 'description', 'cashier_role']
def form_valid(self, form):
response = super().form_valid(form)
PaymentType.create_defaults(self.object)
return response
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['campaigns'] = self.object.campaign_set.all().order_by('-date_start')
return super().get_context_data(**kwargs)
regie_detail = RegieDetailView.as_view()
class RegieParametersView(DetailView):
template_name = 'lingo/invoicing/manager_regie_parameters.html'
model = Regie
def get_context_data(self, **kwargs):
kwargs['regie'] = self.object
kwargs['agendas'] = Agenda.objects.filter(regie=self.object)
has_related_objects = False
if self.object.campaign_set.exists():
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_parameters = RegieParametersView.as_view()
class RegieEditView(UpdateView):
template_name = 'lingo/invoicing/manager_regie_form.html'
model = Regie
form_class = RegieForm
def get_success_url(self):
return reverse('lingo-manager-invoicing-regie-parameters', args=[self.object.pk])
regie_edit = RegieEditView.as_view()
class RegieCountersEditView(UpdateView):
template_name = 'lingo/invoicing/manager_regie_form.html'
template_name = 'lingo/invoicing/manager_regie_counters_form.html'
model = Regie
fields = [
'counter_name',
'invoice_number_format',
'payment_number_format',
'credit_number_format',
'refund_number_format',
]
def get_success_url(self):
return '%s#open:counters' % reverse('lingo-manager-invoicing-regie-parameters', args=[self.object.pk])
regie_counters_edit = RegieCountersEditView.as_view()
class RegiePublishingEditView(UpdateView):
template_name = 'lingo/invoicing/manager_regie_publishing_form.html'
model = Regie
form_class = RegiePublishingForm
def get_success_url(self):
return '%s#open:publishing' % reverse(
'lingo-manager-invoicing-regie-parameters', args=[self.object.pk]
)
regie_publishing_edit = RegiePublishingEditView.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 RegieExport(DetailView):
model = Regie
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
attachment = 'attachment; filename="export_regie_{}_{}.json"'.format(
self.get_object().slug, today.strftime('%Y%m%d')
)
response['Content-Disposition'] = attachment
json.dump({'regies': [self.get_object().export_json()]}, response, indent=2)
return response
regie_export = RegieExport.as_view()
class PaymentTypeAddView(CreateView):
template_name = 'lingo/invoicing/manager_payment_type_form.html'
model = PaymentType
fields = ['label']
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs.pop('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()
if not kwargs.get('instance'):
kwargs['instance'] = self.model()
kwargs['instance'].regie = self.regie
return kwargs
def get_success_url(self):
return '%s#open:payment-types' % reverse(
'lingo-manager-invoicing-regie-parameters', args=[self.regie.pk]
)
payment_type_add = PaymentTypeAddView.as_view()
class PaymentTypeEditView(UpdateView):
template_name = 'lingo/invoicing/manager_payment_type_form.html'
model = PaymentType
form_class = PaymentTypeForm
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs.pop('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_queryset(self):
return PaymentType.objects.filter(regie=self.regie)
def get_success_url(self):
return '%s#open:payment-types' % reverse(
'lingo-manager-invoicing-regie-parameters', args=[self.regie.pk]
)
payment_type_edit = PaymentTypeEditView.as_view()
class PaymentTypeDeleteView(DeleteView):
template_name = 'lingo/manager_confirm_delete.html'
model = PaymentType
def dispatch(self, request, *args, **kwargs):
self.regie_pk = kwargs.pop('regie_pk')
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return PaymentType.objects.filter(regie=self.regie_pk, payment__isnull=True)
def get_success_url(self):
return '%s#open:payment-types' % reverse(
'lingo-manager-invoicing-regie-parameters', args=[self.regie_pk]
)
payment_type_delete = PaymentTypeDeleteView.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',
'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 = JournalLine.objects.filter(
status='error', error_status='', pool__campaign__regie=self.regie
).values(*fields)
qs2 = (
InjectedLine.objects.filter(journalline__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'] = JournalLine(
user_first_name=line['user_first_name'], user_last_name=line['user_last_name']
).user_name
line['payer_name'] = JournalLine(
payer_first_name=line['payer_first_name'], payer_last_name=line['payer_last_name']
).payer_name
line['error_display'] = JournalLine(
status=line['status'], pricing_data=line['pricing_data']
).get_error_display()
line['campaign_id'] = pools[line['pool_id']].campaign_id
line['chrono_event_url'] = JournalLine(event=line['event']).get_chrono_event_url()
return context
non_invoiced_line_list = NonInvoicedLineListView.as_view()
class RegieInvoiceListView(ListView):
template_name = 'lingo/invoicing/manager_invoice_list.html'
paginate_by = 100
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.full = bool('full' in request.GET)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
invoice_queryset = (
Invoice.objects.filter(
Q(pool__isnull=True) | Q(pool__campaign__finalized=True),
regie=self.regie,
cancelled_at__isnull=True,
)
.prefetch_related('pool')
.order_by('-created_at')
)
if self.full:
payment_queryset = InvoiceLinePayment.objects.select_related('payment__payment_type')
invoice_queryset = invoice_queryset.prefetch_related(
Prefetch('lines', queryset=InvoiceLine.objects.all().order_by('pk')),
Prefetch('lines__invoicelinepayment_set', queryset=payment_queryset),
)
self.filterset = RegieInvoiceFilterSet(data=self.request.GET or None, queryset=invoice_queryset)
return self.filterset.qs
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['filterset'] = self.filterset
return super().get_context_data(**kwargs)
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
context = self.get_context_data()
if 'csv' in request.GET and self.filterset.form.is_valid():
return self.csv(request, context)
return self.render_to_response(context)
def csv(self, request, context):
response = HttpResponse(content_type='text/csv')
if self.full:
response['Content-Disposition'] = 'attachment; filename="invoices-full.csv"'
else:
response['Content-Disposition'] = 'attachment; filename="invoices.csv"'
writer = csv.writer(response)
headers = [
_('Number'),
_('Payer ID'),
_('Payer first name'),
_('Payer last name'),
_('Publication date'),
_('Payment deadline'),
_('Due date'),
_('Demat'),
_('Direct debit'),
]
if self.full:
headers += [
_('Description'),
_('Unit amount'),
_('Quantity'),
]
headers += [
_('Total due'),
]
if self.full:
headers += [
_('Payment type'),
]
headers += [
_('Paid amount'),
_('Status'),
]
# headers
writer.writerow(headers)
for invoice in self.object_list:
paid_status = _('Not paid')
if invoice.remaining_amount > 0 and invoice.paid_amount > 0:
paid_status = _('Partially paid')
elif invoice.remaining_amount == 0:
paid_status = _('Paid')
if not self.full:
writer.writerow(
[
invoice.formatted_number,
invoice.payer_external_id,
invoice.payer_first_name,
invoice.payer_last_name,
invoice.date_publication.isoformat(),
invoice.date_payment_deadline.isoformat(),
invoice.date_due.isoformat(),
yesno(invoice.payer_demat),
yesno(invoice.payer_direct_debit),
invoice.total_amount,
invoice.paid_amount,
paid_status,
]
)
continue
for line in invoice.lines.all():
if line.total_amount == 0:
continue
paid_status = _('Not paid')
if line.remaining_amount > 0 and line.paid_amount > 0:
paid_status = _('Partially paid')
elif line.remaining_amount == 0:
paid_status = _('Paid')
payment_types = {p.payment.payment_type.label for p in line.invoicelinepayment_set.all()}
writer.writerow(
[
invoice.formatted_number,
invoice.payer_external_id,
invoice.payer_first_name,
invoice.payer_last_name,
invoice.date_publication.isoformat(),
invoice.date_payment_deadline.isoformat(),
invoice.date_due.isoformat(),
yesno(invoice.payer_demat),
yesno(invoice.payer_direct_debit),
line.label,
line.unit_amount,
line.quantity,
line.total_amount,
', '.join(sorted(payment_types)),
line.paid_amount,
paid_status,
]
)
return response
regie_invoice_list = RegieInvoiceListView.as_view()
class RegieInvoicePDFView(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'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
Q(pool__isnull=True) | Q(pool__campaign__finalized=True),
regie=self.regie,
cancelled_at__isnull=True,
)
)
regie_invoice_pdf = RegieInvoicePDFView.as_view()
class RegieInvoicePaymentsPDFView(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'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
Q(pool__isnull=True) | Q(pool__campaign__finalized=True),
regie=self.regie,
remaining_amount=0,
cancelled_at__isnull=True,
)
)
def html(self):
return self.object.payments_html()
def get_filename(self):
return 'A-%s' % self.object.formatted_number
regie_invoice_payments_pdf = RegieInvoicePaymentsPDFView.as_view()
class RegieInvoiceLineListView(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.invoice = get_object_or_404(
Invoice,
Q(pool__isnull=True) | Q(pool__campaign__finalized=True),
pk=kwargs['invoice_pk'],
regie=self.regie,
cancelled_at__isnull=True,
)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.invoice.get_grouped_and_ordered_lines()
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['invoice'] = self.invoice
return super().get_context_data(**kwargs)
regie_invoice_line_list = RegieInvoiceLineListView.as_view()
class RegiePaymentListView(ListView):
template_name = 'lingo/invoicing/manager_payment_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):
invoice_line_payment_queryset = InvoiceLinePayment.objects.select_related('line__invoice').order_by(
'created_at'
)
self.filterset = RegiePaymentFilterSet(
data=self.request.GET or None,
queryset=Payment.objects.filter(regie=self.regie)
.prefetch_related(
'payment_type',
Prefetch(
'invoicelinepayment_set',
queryset=invoice_line_payment_queryset,
to_attr='prefetched_invoicelinepayments',
),
)
.order_by('-created_at'),
regie=self.regie,
)
return self.filterset.qs
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['filterset'] = self.filterset
return super().get_context_data(**kwargs)
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
context = self.get_context_data()
if 'csv' in request.GET and self.filterset.form.is_valid():
return self.csv(request, context)
return self.render_to_response(context)
def csv(self, request, context):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="payments.csv"'
writer = csv.writer(response)
# headers
headers = [
_('Number'),
_('Invoice number'),
_('Date'),
_('Payer ID'),
_('Payer first name'),
_('Payer last name'),
_('Payment type'),
_('Amount assigned'),
_('Total amount'),
] + [v for k, v in PAYMENT_INFO]
writer.writerow(headers)
for payment in self.object_list:
for invoice_payment in payment.get_invoice_payments():
writer.writerow(
[
payment.formatted_number,
invoice_payment.invoice.formatted_number,
payment.created_at.date().isoformat(),
invoice_payment.invoice.payer_external_id,
invoice_payment.invoice.payer_first_name,
invoice_payment.invoice.payer_last_name,
payment.payment_type.label,
invoice_payment.amount,
payment.amount,
]
+ [payment.payment_info.get(k) or '' for k, v in PAYMENT_INFO]
)
return response
regie_payment_list = RegiePaymentListView.as_view()
class RegiePaymentPDFView(PDFMixin, DetailView):
pk_url_kwarg = 'payment_pk'
model = Payment
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)
regie_payment_pdf = RegiePaymentPDFView.as_view()
class RegieCreditListView(ListView):
template_name = 'lingo/invoicing/manager_credit_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):
credit_queryset = Credit.objects.filter(regie=self.regie).order_by('-created_at')
self.filterset = RegieCreditFilterSet(data=self.request.GET or None, queryset=credit_queryset)
return self.filterset.qs
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['filterset'] = self.filterset
return super().get_context_data(**kwargs)
regie_credit_list = RegieCreditListView.as_view()
class RegieCreditLineListView(ListView):
template_name = 'lingo/invoicing/manager_credit_lines.html'
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.credit = get_object_or_404(
Credit,
pk=kwargs['credit_pk'],
regie=self.regie,
)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.credit.get_grouped_and_ordered_lines()
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['credit'] = self.credit
return super().get_context_data(**kwargs)
regie_credit_line_list = RegieCreditLineListView.as_view()
class RegieCreditPDFView(PDFMixin, DetailView):
pk_url_kwarg = 'credit_pk'
model = Credit
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,
)
)
regie_credit_pdf = RegieCreditPDFView.as_view()
class RegieRefundListView(ListView):
template_name = 'lingo/invoicing/manager_refund_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):
refund_queryset = Refund.objects.filter(regie=self.regie).order_by('-created_at')
self.filterset = RegieRefundFilterSet(data=self.request.GET or None, queryset=refund_queryset)
return self.filterset.qs
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['filterset'] = self.filterset
return super().get_context_data(**kwargs)
regie_refund_list = RegieRefundListView.as_view()