864 lines
29 KiB
Python
864 lines
29 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 django_filters
|
|
from django import forms
|
|
from django.db.models import Q
|
|
from django.utils.translation import gettext_lazy as _
|
|
from gadjo.forms.widgets import MultiSelectWidget
|
|
|
|
from lingo.agendas.models import Agenda
|
|
from lingo.invoicing.models import (
|
|
Campaign,
|
|
DraftInvoice,
|
|
DraftInvoiceLine,
|
|
DraftJournalLine,
|
|
Invoice,
|
|
InvoiceLine,
|
|
InvoicePayment,
|
|
JournalLine,
|
|
Payer,
|
|
Payment,
|
|
PaymentType,
|
|
Regie,
|
|
)
|
|
from lingo.utils.wcs import get_wcs_options
|
|
|
|
|
|
class ExportForm(forms.Form):
|
|
regies = forms.BooleanField(label=_('Regies'), required=False, initial=True)
|
|
payers = forms.BooleanField(label=_('Payers'), 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',
|
|
'payer',
|
|
'cashier_role',
|
|
]
|
|
|
|
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 PaymentTypeForm(forms.ModelForm):
|
|
class Meta:
|
|
model = PaymentType
|
|
fields = ['label', 'slug', 'disabled']
|
|
|
|
def clean_slug(self):
|
|
slug = self.cleaned_data['slug']
|
|
|
|
if self.instance.regie.paymenttype_set.filter(slug=slug).exclude(pk=self.instance.pk).exists():
|
|
raise forms.ValidationError(_('Another payment type exists with the same identifier.'))
|
|
|
|
return slug
|
|
|
|
|
|
class CampaignForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Campaign
|
|
fields = ['label', 'date_start', 'date_end', 'injected_lines', 'agendas']
|
|
widgets = {
|
|
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
'agendas': MultiSelectWidget,
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields['agendas'].queryset = self.instance.regie.agenda_set.all()
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
|
|
if 'date_start' in cleaned_data and 'date_end' in cleaned_data:
|
|
if cleaned_data['date_end'] <= cleaned_data['date_start']:
|
|
self.add_error('date_end', _('End date must be greater than start date.'))
|
|
elif 'agendas' in cleaned_data:
|
|
new_date_start = cleaned_data['date_start']
|
|
new_date_end = cleaned_data['date_end']
|
|
new_agendas = cleaned_data['agendas']
|
|
overlapping_qs = (
|
|
Campaign.objects.filter(regie=self.instance.regie)
|
|
.exclude(pk=self.instance.pk)
|
|
.extra(
|
|
where=['(date_start, date_end) OVERLAPS (%s, %s)'],
|
|
params=[new_date_start, new_date_end],
|
|
)
|
|
)
|
|
for agenda in new_agendas:
|
|
if overlapping_qs.filter(agendas=agenda).exists():
|
|
self.add_error(
|
|
None,
|
|
_('Agenda "%s" has already a campaign overlapping this period.') % agenda.label,
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
def save(self):
|
|
super().save(commit=False)
|
|
|
|
if self.instance._state.adding:
|
|
# init date fields
|
|
self.instance.date_publication = self.instance.date_end
|
|
self.instance.date_payment_deadline = self.instance.date_end
|
|
self.instance.date_due = self.instance.date_end
|
|
self.instance.date_debit = self.instance.date_end
|
|
elif self.instance.pool_set.exists():
|
|
self.instance.mark_as_invalid(commit=False)
|
|
|
|
self.instance.save()
|
|
self._save_m2m()
|
|
|
|
return self.instance
|
|
|
|
|
|
class CampaignDatesForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Campaign
|
|
fields = ['date_publication', 'date_payment_deadline', 'date_due', 'date_debit']
|
|
widgets = {
|
|
'date_publication': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
'date_payment_deadline': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
'date_due': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
'date_debit': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
}
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
|
|
if (
|
|
'date_publication' in cleaned_data
|
|
and 'date_payment_deadline' in cleaned_data
|
|
and 'date_due' in cleaned_data
|
|
):
|
|
if cleaned_data['date_publication'] > cleaned_data['date_payment_deadline']:
|
|
self.add_error(
|
|
'date_payment_deadline', _('Payment deadline must be greater than publication date.')
|
|
)
|
|
elif cleaned_data['date_payment_deadline'] > cleaned_data['date_due']:
|
|
self.add_error('date_due', _('Due date must be greater than payment deadline.'))
|
|
return cleaned_data
|
|
|
|
def save(self):
|
|
super().save()
|
|
|
|
draft_invoice_qs = DraftInvoice.objects.filter(pool__campaign=self.instance)
|
|
invoice_qs = Invoice.objects.filter(pool__campaign=self.instance)
|
|
|
|
for qs in [draft_invoice_qs, invoice_qs]:
|
|
qs.update(
|
|
date_publication=self.instance.date_publication,
|
|
date_payment_deadline=self.instance.date_payment_deadline,
|
|
date_due=self.instance.date_due,
|
|
)
|
|
qs.filter(payer_direct_debit=True).update(date_debit=self.instance.date_debit)
|
|
|
|
return self.instance
|
|
|
|
|
|
class AgendaFieldsFilterSetMixin:
|
|
def _init_agenda_fields(self, line_model, invoice_queryset):
|
|
lines = line_model.objects.filter(invoice__in=invoice_queryset).values('slug', 'details__agenda')
|
|
agenda_slugs = set()
|
|
for line in lines:
|
|
if not line['details__agenda'] and '@' not in line['slug']:
|
|
continue
|
|
agenda_slugs.add(line['details__agenda'] or line['slug'].split('@')[0])
|
|
# get agendas from slugs
|
|
agendas = Agenda.objects.filter(slug__in=agenda_slugs)
|
|
# and init agenda filter choices
|
|
self.filters['agenda'].field.choices = [(a.slug, a.label) for a in agendas]
|
|
# get line details to build event filter choices
|
|
agenda_labels_by_slug = {a.slug: a.label for a in agendas}
|
|
lines = line_model.objects.filter(invoice__in=invoice_queryset).values(
|
|
'slug', 'label', 'details__primary_event', 'details__agenda'
|
|
)
|
|
events = set()
|
|
for line in lines:
|
|
# build a slug with agenda slug and event slug
|
|
slug = (
|
|
'%s@%s' % (line['details__agenda'], line['details__primary_event'])
|
|
if line['details__agenda']
|
|
else line['slug']
|
|
)
|
|
if '@' not in slug:
|
|
# no information about agenda, injected line ?
|
|
continue
|
|
agenda_slug = slug.split('@')[0]
|
|
if agenda_slug not in agenda_labels_by_slug:
|
|
# unknown agenda slug
|
|
continue
|
|
events.add(
|
|
(
|
|
slug,
|
|
'%s / %s' % (agenda_labels_by_slug[agenda_slug], line['label']),
|
|
agenda_labels_by_slug[agenda_slug],
|
|
line['label'],
|
|
)
|
|
)
|
|
# build event filter choices
|
|
events = sorted(list(events), key=lambda e: (e[2], e[3]))
|
|
self.filters['event'].field.choices = [(e[0], e[1]) for e in events]
|
|
|
|
def filter_agenda(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
line_model = InvoiceLine
|
|
if hasattr(self, 'pool') and self.pool.draft:
|
|
line_model = DraftInvoiceLine
|
|
lines = line_model.objects.filter(
|
|
Q(details__agenda=value) | Q(slug__startswith='%s@' % value)
|
|
).values('invoice')
|
|
return queryset.filter(pk__in=lines)
|
|
|
|
def filter_event(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
line_model = InvoiceLine
|
|
if hasattr(self, 'pool') and self.pool.draft:
|
|
line_model = DraftInvoiceLine
|
|
agenda_slug, event_slug = value.split('@')
|
|
lines = line_model.objects.filter(
|
|
Q(details__agenda=agenda_slug, details__primary_event=event_slug) | Q(slug=value)
|
|
).values('invoice')
|
|
return queryset.filter(pk__in=lines)
|
|
|
|
|
|
class AbstractInvoiceFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet):
|
|
# for Invoice
|
|
number = django_filters.CharFilter(
|
|
label=_('Invoice number'),
|
|
field_name='formatted_number',
|
|
lookup_expr='contains',
|
|
)
|
|
# for DraftInvoice
|
|
pk = django_filters.NumberFilter(
|
|
label=_('Invoice number'),
|
|
)
|
|
payment_number = django_filters.CharFilter(
|
|
label=_('Payment number'),
|
|
method='filter_payment_number',
|
|
)
|
|
payer_external_id = django_filters.CharFilter(
|
|
label=_('Payer (external ID)'),
|
|
)
|
|
payer_first_name = django_filters.CharFilter(
|
|
label=_('Payer first name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
payer_last_name = django_filters.CharFilter(
|
|
label=_('Payer last name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
payer_demat = django_filters.BooleanFilter(
|
|
label=_('Payer demat'),
|
|
)
|
|
payer_direct_debit = django_filters.BooleanFilter(
|
|
label=_('Payer direct debit'),
|
|
)
|
|
user_external_id = django_filters.CharFilter(
|
|
label=_('User (external ID)'),
|
|
method='filter_user_external_id',
|
|
)
|
|
user_first_name = django_filters.CharFilter(
|
|
label=_('User first name'),
|
|
method='filter_user_first_name',
|
|
)
|
|
user_last_name = django_filters.CharFilter(
|
|
label=_('User last name'),
|
|
method='filter_user_last_name',
|
|
)
|
|
total_amount_min = django_filters.LookupChoiceFilter(
|
|
label=_('Total amount min'),
|
|
field_name='total_amount',
|
|
field_class=forms.DecimalField,
|
|
empty_label=None,
|
|
lookup_choices=[
|
|
('gt', '>'),
|
|
('gte', '>='),
|
|
],
|
|
)
|
|
total_amount_max = django_filters.LookupChoiceFilter(
|
|
label=_('Total amount max'),
|
|
field_name='total_amount',
|
|
field_class=forms.DecimalField,
|
|
empty_label=None,
|
|
lookup_choices=[
|
|
('lt', '<'),
|
|
('lte', '<='),
|
|
],
|
|
)
|
|
paid = django_filters.ChoiceFilter(
|
|
label=_('Paid'),
|
|
widget=forms.RadioSelect,
|
|
empty_label=_('all'),
|
|
choices=[
|
|
('yes', _('Totally')),
|
|
('partially', _('Partially')),
|
|
('no', _('No')),
|
|
],
|
|
method='filter_paid',
|
|
)
|
|
agenda = django_filters.ChoiceFilter(
|
|
label=_('Activity'),
|
|
empty_label=_('all'),
|
|
method='filter_agenda',
|
|
)
|
|
event = django_filters.ChoiceFilter(
|
|
label=_('Event'),
|
|
empty_label=_('all'),
|
|
method='filter_event',
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.pool = kwargs.pop('pool')
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if self.pool.draft:
|
|
del self.filters['number']
|
|
del self.filters['payment_number']
|
|
del self.filters['paid']
|
|
else:
|
|
del self.filters['pk']
|
|
|
|
line_model = InvoiceLine
|
|
if self.pool.draft:
|
|
line_model = DraftInvoiceLine
|
|
self._init_agenda_fields(line_model, self.queryset)
|
|
|
|
def filter_payment_number(self, queryset, name, value):
|
|
return queryset.filter(
|
|
pk__in=InvoicePayment.objects.filter(payment__formatted_number__contains=value).values('invoice')
|
|
)
|
|
|
|
def filter_user_external_id(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
line_model = InvoiceLine
|
|
if self.pool.draft:
|
|
line_model = DraftInvoiceLine
|
|
lines = line_model.objects.filter(user_external_id=value).values('invoice')
|
|
return queryset.filter(pk__in=lines)
|
|
|
|
def filter_user_first_name(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
line_model = InvoiceLine
|
|
if self.pool.draft:
|
|
line_model = DraftInvoiceLine
|
|
lines = line_model.objects.filter(user_first_name__icontains=value).values('invoice')
|
|
return queryset.filter(pk__in=lines)
|
|
|
|
def filter_user_last_name(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
line_model = InvoiceLine
|
|
if self.pool.draft:
|
|
line_model = DraftInvoiceLine
|
|
lines = line_model.objects.filter(user_last_name__icontains=value).values('invoice')
|
|
return queryset.filter(pk__in=lines)
|
|
|
|
def filter_paid(self, queryset, name, value):
|
|
if value == 'yes':
|
|
return queryset.filter(remaining_amount=0)
|
|
if value == 'partially':
|
|
return queryset.filter(remaining_amount__gt=0, paid_amount__gt=0)
|
|
if value == 'no':
|
|
return queryset.filter(paid_amount=0)
|
|
return queryset
|
|
|
|
|
|
class DraftInvoiceFilterSet(AbstractInvoiceFilterSet):
|
|
class Meta:
|
|
model = DraftInvoice
|
|
fields = []
|
|
|
|
|
|
class InvoiceFilterSet(AbstractInvoiceFilterSet):
|
|
class Meta:
|
|
model = Invoice
|
|
fields = []
|
|
|
|
|
|
class AbstractJournalLineFilterSet(django_filters.FilterSet):
|
|
# for JournalLine
|
|
invoice_number = django_filters.CharFilter(
|
|
label=_('Invoice number'),
|
|
field_name='invoice_line__invoice__formatted_number',
|
|
lookup_expr='contains',
|
|
)
|
|
# for DraftJournalLine
|
|
invoice_id = django_filters.NumberFilter(
|
|
label=_('Invoice number'),
|
|
field_name='invoice_line__invoice_id',
|
|
)
|
|
invoice_line = django_filters.NumberFilter(
|
|
label=_('Invoice line'),
|
|
)
|
|
payer_external_id = django_filters.CharFilter(
|
|
label=_('Payer (external ID)'),
|
|
)
|
|
payer_first_name = django_filters.CharFilter(
|
|
label=_('Payer first name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
payer_last_name = django_filters.CharFilter(
|
|
label=_('Payer last name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
payer_demat = django_filters.BooleanFilter(
|
|
label=_('Payer demat'),
|
|
)
|
|
payer_direct_debit = django_filters.BooleanFilter(
|
|
label=_('Payer direct debit'),
|
|
)
|
|
user_external_id = django_filters.CharFilter(
|
|
label=_('User (external ID)'),
|
|
)
|
|
user_first_name = django_filters.CharFilter(
|
|
label=_('User first name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
user_last_name = django_filters.CharFilter(
|
|
label=_('User last name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
agenda = django_filters.ChoiceFilter(
|
|
label=_('Activity'),
|
|
empty_label=_('all'),
|
|
method='filter_agenda',
|
|
)
|
|
event = django_filters.ChoiceFilter(
|
|
label=_('Event'),
|
|
empty_label=_('all'),
|
|
method='filter_event',
|
|
)
|
|
status = django_filters.ChoiceFilter(
|
|
label=_('Status'),
|
|
widget=forms.RadioSelect,
|
|
empty_label=_('all'),
|
|
choices=[
|
|
('success', _('Success')),
|
|
('success_injected', _('Success (Injected)')),
|
|
('warning', _('Warning')),
|
|
('error', _('Error')),
|
|
('error_todo', _('Error (To treat)')),
|
|
('error_ignored', _('Error (Ignored)')),
|
|
('error_fixed', _('Error (Fixed)')),
|
|
],
|
|
method='filter_status',
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.pool = kwargs.pop('pool')
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if self.pool.draft:
|
|
del self.filters['invoice_number']
|
|
else:
|
|
del self.filters['invoice_id']
|
|
|
|
agenda_slugs = list(set(self.queryset.values_list('event__agenda', flat=True)))
|
|
agendas = Agenda.objects.filter(slug__in=agenda_slugs)
|
|
self.filters['agenda'].field.choices = [(a.slug, a.label) for a in agendas]
|
|
agenda_labels_by_slug = {a.slug: a.label for a in agendas}
|
|
lines = self.queryset.values('event__agenda', 'event__slug', 'event__primary_event', 'label')
|
|
events = set()
|
|
for line in lines:
|
|
if line['event__agenda'] not in agenda_labels_by_slug:
|
|
continue
|
|
events.add(
|
|
(
|
|
'%s@%s' % (line['event__agenda'], line['event__primary_event'] or line['event__slug']),
|
|
'%s / %s' % (agenda_labels_by_slug.get(line['event__agenda']), line['label']),
|
|
agenda_labels_by_slug.get(line['event__agenda']),
|
|
line['label'],
|
|
)
|
|
)
|
|
events = sorted(list(events), key=lambda e: (e[2], e[3]))
|
|
self.filters['event'].field.choices = [(e[0], e[1]) for e in events]
|
|
|
|
def filter_agenda(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
return queryset.filter(event__agenda=value)
|
|
|
|
def filter_event(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
agenda_slug, event_slug = value.split('@')
|
|
return queryset.filter(
|
|
Q(event__primary_event=event_slug) | Q(event__slug=event_slug), event__agenda=agenda_slug
|
|
)
|
|
|
|
def filter_status(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
if value == 'success_injected':
|
|
return queryset.filter(status='success', from_injected_line__isnull=False)
|
|
if value == 'error_todo':
|
|
return queryset.filter(status='error', error_status='')
|
|
if value == 'error_ignored':
|
|
return queryset.filter(status='error', error_status='ignored')
|
|
if value == 'error_fixed':
|
|
return queryset.filter(status='error', error_status='fixed')
|
|
return queryset.filter(status=value)
|
|
|
|
|
|
class DraftJournalLineFilterSet(AbstractJournalLineFilterSet):
|
|
class Meta:
|
|
model = DraftJournalLine
|
|
fields = []
|
|
|
|
|
|
class JournalLineFilterSet(AbstractJournalLineFilterSet):
|
|
class Meta:
|
|
model = JournalLine
|
|
fields = []
|
|
|
|
|
|
class DateRangeWidget(django_filters.widgets.DateRangeWidget):
|
|
def __init__(self, attrs=None):
|
|
widgets = (
|
|
forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
)
|
|
super(django_filters.widgets.SuffixedMultiWidget, self).__init__(widgets, attrs)
|
|
|
|
|
|
class DateRangeField(django_filters.fields.DateRangeField):
|
|
widget = DateRangeWidget
|
|
|
|
|
|
class DateFromToRangeFilter(django_filters.DateFromToRangeFilter):
|
|
field_class = DateRangeField
|
|
|
|
|
|
class RegieInvoiceFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet):
|
|
number = django_filters.CharFilter(
|
|
label=_('Invoice number'),
|
|
field_name='formatted_number',
|
|
lookup_expr='contains',
|
|
)
|
|
created_at = DateFromToRangeFilter(
|
|
label=_('Creation date'),
|
|
field_name='created_at',
|
|
)
|
|
date_payment_deadline = DateFromToRangeFilter(
|
|
label=_('Payment deadline'),
|
|
field_name='date_payment_deadline',
|
|
)
|
|
date_due = DateFromToRangeFilter(
|
|
label=_('Due date'),
|
|
field_name='date_due',
|
|
)
|
|
payment_number = django_filters.CharFilter(
|
|
label=_('Payment number'),
|
|
method='filter_payment_number',
|
|
)
|
|
payer_external_id = django_filters.CharFilter(
|
|
label=_('Payer (external ID)'),
|
|
)
|
|
payer_first_name = django_filters.CharFilter(
|
|
label=_('Payer first name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
payer_last_name = django_filters.CharFilter(
|
|
label=_('Payer last name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
payer_demat = django_filters.BooleanFilter(
|
|
label=_('Payer demat'),
|
|
)
|
|
payer_direct_debit = django_filters.BooleanFilter(
|
|
label=_('Payer direct debit'),
|
|
)
|
|
user_external_id = django_filters.CharFilter(
|
|
label=_('User (external ID)'),
|
|
method='filter_user_external_id',
|
|
)
|
|
user_first_name = django_filters.CharFilter(
|
|
label=_('User first name'),
|
|
method='filter_user_first_name',
|
|
)
|
|
user_last_name = django_filters.CharFilter(
|
|
label=_('User last name'),
|
|
method='filter_user_last_name',
|
|
)
|
|
total_amount_min = django_filters.LookupChoiceFilter(
|
|
label=_('Total amount min'),
|
|
field_name='total_amount',
|
|
field_class=forms.DecimalField,
|
|
empty_label=None,
|
|
lookup_choices=[
|
|
('gt', '>'),
|
|
('gte', '>='),
|
|
],
|
|
)
|
|
total_amount_max = django_filters.LookupChoiceFilter(
|
|
label=_('Total amount max'),
|
|
field_name='total_amount',
|
|
field_class=forms.DecimalField,
|
|
empty_label=None,
|
|
lookup_choices=[
|
|
('lt', '<'),
|
|
('lte', '<='),
|
|
],
|
|
)
|
|
paid = django_filters.ChoiceFilter(
|
|
label=_('Paid'),
|
|
widget=forms.RadioSelect,
|
|
empty_label=_('all'),
|
|
choices=[
|
|
('yes', _('Totally')),
|
|
('partially', _('Partially')),
|
|
('no', _('No')),
|
|
],
|
|
method='filter_paid',
|
|
)
|
|
agenda = django_filters.ChoiceFilter(
|
|
label=_('Activity'),
|
|
empty_label=_('all'),
|
|
method='filter_agenda',
|
|
)
|
|
event = django_filters.ChoiceFilter(
|
|
label=_('Event'),
|
|
empty_label=_('all'),
|
|
method='filter_event',
|
|
)
|
|
|
|
class Meta:
|
|
model = Invoice
|
|
fields = []
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self._init_agenda_fields(InvoiceLine, self.queryset)
|
|
|
|
def filter_payment_number(self, queryset, name, value):
|
|
return queryset.filter(
|
|
pk__in=InvoicePayment.objects.filter(payment__formatted_number__contains=value).values('invoice')
|
|
)
|
|
|
|
def filter_user_external_id(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
lines = InvoiceLine.objects.filter(user_external_id=value).values('invoice')
|
|
return queryset.filter(pk__in=lines)
|
|
|
|
def filter_user_first_name(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
lines = InvoiceLine.objects.filter(user_first_name__icontains=value).values('invoice')
|
|
return queryset.filter(pk__in=lines)
|
|
|
|
def filter_user_last_name(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
lines = InvoiceLine.objects.filter(user_last_name__icontains=value).values('invoice')
|
|
return queryset.filter(pk__in=lines)
|
|
|
|
def filter_paid(self, queryset, name, value):
|
|
if value == 'yes':
|
|
return queryset.filter(remaining_amount=0)
|
|
if value == 'partially':
|
|
return queryset.filter(remaining_amount__gt=0, paid_amount__gt=0)
|
|
if value == 'no':
|
|
return queryset.filter(paid_amount=0)
|
|
return queryset
|
|
|
|
|
|
class RegiePaymentFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet):
|
|
number = django_filters.CharFilter(
|
|
label=_('Payment number'),
|
|
field_name='formatted_number',
|
|
lookup_expr='contains',
|
|
)
|
|
created_at = DateFromToRangeFilter(
|
|
label=_('Date'),
|
|
field_name='created_at',
|
|
)
|
|
invoice_number = django_filters.CharFilter(
|
|
label=_('Invoice number'),
|
|
method='filter_invoice_number',
|
|
)
|
|
payer_external_id = django_filters.CharFilter(
|
|
label=_('Payer (external ID)'),
|
|
)
|
|
payer_first_name = django_filters.CharFilter(
|
|
label=_('Payer first name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
payer_last_name = django_filters.CharFilter(
|
|
label=_('Payer last name'),
|
|
lookup_expr='icontains',
|
|
)
|
|
payment_type = django_filters.ChoiceFilter(
|
|
label=_('Payment type'),
|
|
widget=forms.RadioSelect,
|
|
empty_label=_('all'),
|
|
)
|
|
amount_min = django_filters.LookupChoiceFilter(
|
|
label=_('Amount min'),
|
|
field_name='amount',
|
|
field_class=forms.DecimalField,
|
|
empty_label=None,
|
|
lookup_choices=[
|
|
('gt', '>'),
|
|
('gte', '>='),
|
|
],
|
|
)
|
|
amount_max = django_filters.LookupChoiceFilter(
|
|
label=_('Amount max'),
|
|
field_name='amount',
|
|
field_class=forms.DecimalField,
|
|
empty_label=None,
|
|
lookup_choices=[
|
|
('lt', '<'),
|
|
('lte', '<='),
|
|
],
|
|
)
|
|
agenda = django_filters.ChoiceFilter(
|
|
label=_('Activity'),
|
|
empty_label=_('all'),
|
|
method='filter_agenda',
|
|
)
|
|
event = django_filters.ChoiceFilter(
|
|
label=_('Event'),
|
|
empty_label=_('all'),
|
|
method='filter_event',
|
|
)
|
|
|
|
class Meta:
|
|
model = Payment
|
|
fields = []
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.regie = kwargs.pop('regie')
|
|
super().__init__(*args, **kwargs)
|
|
self.filters['payment_type'].field.choices = [(t.pk, t) for t in self.regie.paymenttype_set.all()]
|
|
|
|
invoice_queryset = Invoice.objects.filter(
|
|
pk__in=InvoicePayment.objects.filter(payment__in=self.queryset).values('invoice')
|
|
)
|
|
self._init_agenda_fields(InvoiceLine, invoice_queryset)
|
|
|
|
def filter_invoice_number(self, queryset, name, value):
|
|
return queryset.filter(
|
|
pk__in=InvoicePayment.objects.filter(invoice__formatted_number__contains=value).values('payment')
|
|
)
|
|
|
|
def filter_agenda(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
lines = InvoiceLine.objects.filter(
|
|
Q(details__agenda=value) | Q(slug__startswith='%s@' % value)
|
|
).values('invoice')
|
|
return queryset.filter(pk__in=InvoicePayment.objects.filter(invoice__in=lines).values('payment'))
|
|
|
|
def filter_event(self, queryset, name, value):
|
|
if not value:
|
|
return queryset
|
|
agenda_slug, event_slug = value.split('@')
|
|
lines = InvoiceLine.objects.filter(
|
|
Q(details__agenda=agenda_slug, details__primary_event=event_slug) | Q(slug=value)
|
|
).values('invoice')
|
|
return queryset.filter(pk__in=InvoicePayment.objects.filter(invoice__in=lines).values('payment'))
|
|
|
|
|
|
class NewPayerForm(forms.ModelForm):
|
|
carddef_reference = forms.ChoiceField(
|
|
label=_('Linked card model'),
|
|
required=False,
|
|
)
|
|
|
|
class Meta:
|
|
model = Payer
|
|
fields = [
|
|
'label',
|
|
'description',
|
|
'carddef_reference',
|
|
'payer_external_id_prefix',
|
|
'payer_external_id_template',
|
|
'payer_external_id_from_nameid_template',
|
|
]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
card_models = get_wcs_options('/api/cards/@list')
|
|
self.fields['carddef_reference'].choices = [('', '-----')] + card_models
|
|
|
|
|
|
class PayerForm(NewPayerForm):
|
|
class Meta:
|
|
model = Payer
|
|
fields = [
|
|
'label',
|
|
'slug',
|
|
'description',
|
|
'carddef_reference',
|
|
'payer_external_id_prefix',
|
|
'payer_external_id_template',
|
|
'payer_external_id_from_nameid_template',
|
|
]
|
|
|
|
def clean_slug(self):
|
|
slug = self.cleaned_data['slug']
|
|
|
|
if Payer.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
|
|
raise forms.ValidationError(_('Another payer exists with the same identifier.'))
|
|
|
|
return slug
|
|
|
|
|
|
class PayerMappingForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Payer
|
|
fields = []
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if not self.instance.cached_carddef_json:
|
|
return
|
|
for key, label in self.instance.user_variables:
|
|
self.fields[key] = forms.ChoiceField(
|
|
label=label,
|
|
choices=[('', '-----')] + [(k, v) for k, v in self.instance.carddef_fields.items()],
|
|
required=False,
|
|
)
|
|
|
|
def save(self):
|
|
self.instance.user_fields_mapping = {k: self.cleaned_data[k] for k, v in self.instance.user_variables}
|
|
self.instance.save()
|
|
return self.instance
|