437 lines
15 KiB
Python
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
|