lingo/lingo/invoicing/forms.py

437 lines
15 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.utils.translation import gettext_lazy as _
from gadjo.forms.widgets import MultiSelectWidget
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):
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_issue = 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_issue', '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_issue': 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_issue' 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_issue']:
self.add_error('date_issue', _('Issue 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_issue=self.instance.date_issue,
)
qs.filter(payer_direct_debit=True).update(date_debit=self.instance.date_debit)
return self.instance
class AbstractInvoiceFilterSet(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'),
)
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',
)
paid = django_filters.ChoiceFilter(
label=_('Paid'),
widget=forms.RadioSelect,
empty_label=_('all'),
choices=[
('yes', _('Totally')),
('partially', _('Partially')),
('no', _('No')),
],
method='filter_paid',
)
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['paid']
else:
del self.filters['pk']
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 AbstractLineFilterSet(django_filters.FilterSet):
# for InvoiceLine
invoice_number = django_filters.CharFilter(
label=_('Invoice number'),
field_name='invoice__formatted_number',
lookup_expr='contains',
)
# for DraftInvoiceLine
invoice_id = django_filters.NumberFilter(
label=_('Invoice number'),
)
pk = django_filters.NumberFilter(
label=_('PK'),
)
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',
)
status = django_filters.ChoiceFilter(
label=_('Status'),
widget=forms.RadioSelect,
empty_label=_('all'),
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']
status_choices = [
('success', _('Success')),
('success_injected', '%s (%s)' % (_('Success'), _('Injected'))),
('warning', _('Warning')),
('error', _('Error')),
]
if not self.pool.draft:
status_choices += [
('error_todo', _('Error (To treat)')),
('error_ignored', _('Error (Ignored)')),
('error_fixed', _('Error (Fixed)')),
]
self.filters['status'].field.choices = status_choices
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 DraftInvoiceLineFilterSet(AbstractLineFilterSet):
class Meta:
model = DraftInvoiceLine
fields = []
class InvoiceLineFilterSet(AbstractLineFilterSet):
class Meta:
model = InvoiceLine
fields = []
class RegieInvoiceFilterSet(django_filters.FilterSet):
number = django_filters.CharFilter(
label=_('Invoice number'),
field_name='formatted_number',
lookup_expr='contains',
)
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',
)
paid = django_filters.ChoiceFilter(
label=_('Paid'),
widget=forms.RadioSelect,
empty_label=_('all'),
choices=[
('yes', _('Totally')),
('partially', _('Partially')),
('no', _('No')),
],
method='filter_paid',
)
class Meta:
model = Invoice
fields = []
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