barbacompta/eo_gestion/eo_facture/admin.py

456 lines
15 KiB
Python

# barbacompta - invoicing for dummies
# Copyright (C) 2019-2020 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 datetime as dt
import re
from adminsortable2.admin import SortableInlineAdminMixin
from django import http
from django.conf.urls import url
from django.contrib import admin
from django.contrib.admin.options import BaseModelAdmin
from django.contrib.humanize.templatetags.humanize import ordinal
from django.db.models import TextField
from django.forms import Textarea
from django.forms.models import BaseInlineFormSet
from django.shortcuts import render
from django.urls import reverse
from django.utils.html import format_html
from django.utils.six import BytesIO
import eo_gestion.admin
from .. import actions, ods
from . import forms, models, views
from .templatetags.eo_facture import amountformat
class LookupAllowed:
def lookup_allowed(self, *args, **kwargs):
return True
class SelectRelatedMixin:
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related()
class CommonPaymentInline(BaseModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'facture' and request.path.endswith('/add/'):
kwargs['queryset'] = models.Facture.objects.avec_solde()
if db_field.name == 'ligne_banque_pop' and request.path.endswith('/add/'):
kwargs['queryset'] = models.encaissements_avec_solde_non_affecte()
return super().formfield_for_foreignkey(db_field, request, **kwargs)
class PrestationInline(SelectRelatedMixin, admin.TabularInline):
model = models.Prestation
form = forms.PrestationForm
class FactureInline(SelectRelatedMixin, admin.TabularInline):
model = models.Facture
form = forms.FactureForm
class PaymentInlineFormset(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for form in self.forms:
if form.instance.id is None:
field = form.fields["ligne_banque_pop"]
if not hasattr(field, "parent_instance"):
field.queryset = models.encaissements_avec_solde_non_affecte()
field = form.fields["facture"]
if not hasattr(field, "parent_instance"):
field.queryset = models.Facture.objects.avec_solde()
class PaymentInline(SelectRelatedMixin, admin.TabularInline, CommonPaymentInline):
form = forms.PaymentForm
model = models.Payment
list_display = ['facture', 'ligne_banque_pop', 'montant_affecte']
formset = PaymentInlineFormset
extra = 0
class LigneInline(SelectRelatedMixin, SortableInlineAdminMixin, admin.TabularInline):
form = forms.LigneForm
model = models.Ligne
original = False
verbose_name_plural = "Lignes de facture (vous pouvez les réordonner par drag&drop)"
formfield_overrides = {
TextField: {
'widget': Textarea(attrs={'cols': 30, 'rows': 1}),
}
}
class ActiveFilter(admin.SimpleListFilter):
title = 'statut'
parameter_name = 'active'
def value(self):
value = super().value()
return 'True' if value in (None, True) else value
def choices(self, changelist):
choices = list(super().choices(changelist))
return choices[1:] # don't include automatic "All"
def lookups(self, request, model_admin):
return [(True, 'actif'), (False, 'inactif'), ('all', 'tous')]
def queryset(self, request, queryset):
if self.value() == 'all':
return queryset
return queryset.filter(active=bool(self.value() == 'True'))
class ClientAdmin(admin.ModelAdmin):
form = forms.ClientForm
list_display = ['nom', 'adresse', 'email', 'telephone']
list_editable = ['email', 'telephone']
list_filter = [ActiveFilter]
ordering = ['nom']
search_fields = ['nom', 'email']
save_on_top = True
list_select_related = True
raw_id_fields = ['chorus_structure']
def get_readonly_fields(self, request, obj=None):
readonly_fields = super().get_readonly_fields(request, obj=obj)
if obj and obj.chorus_structure:
readonly_fields += ('siret', 'service_code')
return readonly_fields
def save_model(self, request, obj, form, change):
if not obj.id:
obj.creator = request.user
obj.save()
class Media:
css = {'all': ('css/client.css',)}
def show_client(obj):
url = reverse('admin:eo_facture_client_change', args=[obj.client.id])
return format_html('<a href="{0}">{1}</a>', url, obj.client)
show_client.short_description = 'Client'
show_client.admin_order_field = 'client'
class ContratAdmin(LookupAllowed, admin.ModelAdmin):
form = forms.ContratForm
inlines = [
PrestationInline,
]
list_display = [
"__str__",
show_client,
'column_montant',
'pourcentage_facture',
'creation',
'creator',
'tag_list',
]
list_filter = ['tags', 'client']
list_select_related = True
save_on_top = True
search_fields = ['intitule', 'client__nom', 'tags__name']
readonly_fields = ['creator']
list_select_related = True
actions = [actions.export_references_as_fodt]
autocomplete_fields = ['client']
def column_montant(self, obj):
return amountformat(obj.montant())
column_montant.short_description = 'Montant'
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related('prestations', 'factures__lignes', 'tags')
def tag_list(self, obj):
return ', '.join(o.name for o in obj.tags.all())
def save_model(self, request, obj, form, change):
if not obj.id:
obj.creator = request.user
obj.save()
class Media:
css = {'all': ('css/contrat.css',)}
def duplicate(self, request, contract):
contrat = models.Contrat.objects.get(id=contract)
if request.method == 'POST':
form = forms.DuplicateContractForm(request=request, data=request.POST)
if form.is_valid():
return http.HttpResponseRedirect(
reverse('admin:eo_facture_contrat_change', args=[form.cleaned_data['new_contrat'].id])
)
else:
new_intitule = contrat.intitule
new_intitule += ' dupliqué le %s' % dt.datetime.now()
form = forms.DuplicateContractForm(initial={'contrat': contrat.id, 'new_intitule': new_intitule})
context = {
'form': form,
'contrat': contrat,
'app_label': self.model._meta.app_label,
'has_change_permission': self.has_change_permission(request),
'opts': self.model._meta,
}
return render(request, "admin/eo_facture/contrat/duplicate.html", context=context)
def get_urls(self):
urls = super().get_urls()
duplicate_view = self.admin_site.admin_view(self.duplicate)
my_urls = [url(r'^(.+)/duplicate/$', duplicate_view, name='eo_facture_contrat_duplicate')]
return my_urls + urls
def index(facture):
return format_html('{0}', ordinal(facture.index()))
index.short_description = "Ordre"
class FactureAdmin(LookupAllowed, admin.ModelAdmin):
form = forms.FactureForm
inlines = [LigneInline, PaymentInline]
list_display = [
'code',
index,
'show_contrat',
'show_client',
'emission',
'creator',
'column_montant',
'column_montant_ttc',
'column_solde',
'paid',
]
list_filter = ['paid', 'emission', 'creator', 'client']
fields = [
'proforma',
'ordre',
'client',
'contrat',
'intitule',
'numero_engagement',
'notes',
'private_notes',
'taux_tva',
'emission',
'echeance',
'paid',
'solde',
'montant',
'montant_ttc',
'account_on_previous_period',
]
readonly_fields = ['creator', 'solde', 'ordre', 'montant', 'montant_ttc']
date_hierarchy = 'emission'
list_select_related = True
save_on_top = True
search_fields = ['contrat__intitule', 'contrat__client__nom']
list_select_related = True
actions = [actions.export_invoices_as_zip]
autocomplete_fields = ['client']
def column_montant(self, obj):
return amountformat(obj.montant)
column_montant.short_description = 'Montant'
def column_montant_ttc(self, obj):
return amountformat(obj.montant_ttc)
column_montant_ttc.short_description = 'Montant TTC'
def column_solde(self, obj):
return amountformat(obj.solde())
column_solde.short_description = 'Solde'
def get_queryset(self, request):
from django.db import connection
qs = super().get_queryset(request)
qs = qs.prefetch_related("lignes__facture__client")
qs = qs.prefetch_related("payments__ligne_banque_pop")
qs = qs.prefetch_related("contrat__factures")
if connection.vendor == "postgresql":
qs = qs.extra({"year": "EXTRACT(year FROM emission)"})
elif connection.vendor == "sqlite":
qs = qs.extra({"year": "strftime('%Y', emission)"})
else:
qs = qs.extra({"year": "YEAR(emission)"})
qs = qs.order_by("-year", "-proforma", "-ordre")
return qs
def save_model(self, request, obj, form, change):
if not obj.id:
obj.creator = request.user
obj.save()
def add_simple(self, request):
context = {}
if request.method == "POST":
form = forms.RapidFactureForm(request=request, data=request.POST)
if form.is_valid():
return http.HttpResponseRedirect(
reverse('admin:eo_facture_facture_change', args=[form.cleaned_data['facture'].id])
)
else:
form = forms.RapidFactureForm(initial=request.GET)
context = {
'form': form,
'app_label': self.model._meta.app_label,
'has_change_permission': self.has_change_permission(request),
'opts': self.model._meta,
}
return render(request, "admin/eo_facture/facture/add_simple.html", context=context)
def sheet(self, request):
workbook = ods.Workbook()
ws = workbook.add_sheet("Factures")
for i, header_name in enumerate(
['Référence', 'Nom client', 'Date', 'Montant HT', 'Montant TTC', 'TVA']
):
ws.write(0, i, header_name)
qs = self.get_queryset(request)
qs = sorted(qs, key=lambda i: (i.emission.year, i.ordre or -1, i.emission))
for j, facture in enumerate(qs):
ws.write(j + 1, 0, facture.code())
ws.write(j + 1, 1, facture.client.nom)
ws.write(j + 1, 2, facture.emission)
ws.write(j + 1, 3, facture.montant)
ws.write(j + 1, 4, facture.montant_ttc)
ws.write(j + 1, 5, facture.tva)
stream = BytesIO()
workbook.save(stream)
response = http.HttpResponse(
stream.getvalue(), content_type='application/vnd.oasis.opendocument.spreadsheet'
)
response['Content-Disposition'] = 'attachment; filename="factures.ods"'
return response
def get_urls(self):
urls = super().get_urls()
my_urls = [
url(r'^view/([^/]*)', views.facture),
url(
r'^add_simple/$',
self.admin_site.admin_view(self.add_simple),
name='eo_facture_facture_add_simple',
),
url(
r'^(.+)/view_pdf/',
self.admin_site.admin_view(views.facture_pdf),
name='eo_facture_facture_print',
),
url(r'^(.+)/view/$', self.admin_site.admin_view(views.facture), name='eo_facture_facture_html'),
url(
r'^(.+)/send-to-chorus/',
self.admin_site.admin_view(views.send_to_chorus),
name='eo_facture_facture_send_to_chorus',
),
url(
r"^sheet/$",
self.admin_site.admin_view(self.sheet),
name="eo_facture_facture_sheet",
),
url(
r'^(.+)/cancel/',
self.admin_site.admin_view(views.cancel),
name='eo_facture_facture_cancel',
),
]
return my_urls + urls
def show_client(self, obj):
url = reverse('admin:eo_facture_client_change', args=[obj.client.id])
return format_html('<a href="{0}">{1}</a>', url, obj.client)
show_client.short_description = 'Client'
def show_contrat(self, obj):
if obj.contrat:
url = reverse('admin:eo_facture_contrat_change', args=[obj.contrat.id])
return format_html('<a href="{0}">{1}</a>', url, obj.contrat)
return None
show_contrat.short_description = 'Contrat'
# adapt get_object and get_changelist_instance to produce and accept URL
# with facture's code
FACTURE_RE = re.compile('^F(?P<year>20[0-9]{2})(?P<ordre>[0-9]{4})$')
def get_object(self, request, object_id, from_field=None):
m = self.FACTURE_RE.match(object_id)
if m:
queryset = self.get_queryset(request)
model = queryset.model
year = int(m.group('year'))
ordre = int(m.group('ordre'))
try:
return queryset.get(emission__year=year, ordre=ordre)
except model.DoesNotExist:
pass
return super().get_object(request, object_id, from_field=from_field)
def get_changelist_instance(self, request):
changelist = super().get_changelist_instance(request)
changelist.pk_attname = 'pk_or_code'
return changelist
class PaymentAdmin(LookupAllowed, admin.ModelAdmin, CommonPaymentInline):
form = forms.PaymentForm
list_display = ['facture', 'ligne_banque_pop', 'montant_affecte']
list_filter = ['ligne_banque_pop__date_valeur', 'facture', 'facture__contrat', 'facture__contrat__client']
list_select_related = True
class PrestationAdmin(admin.ModelAdmin):
form = forms.PrestationForm
list_select_related = True
admin.site.register(models.Client, ClientAdmin)
admin.site.register(models.Contrat, ContratAdmin)
admin.site.register(models.Facture, FactureAdmin)
admin.site.register(models.Prestation, PrestationAdmin)
eo_gestion.admin.site.register(models.Client, ClientAdmin)
eo_gestion.admin.site.register(models.Contrat, ContratAdmin)
eo_gestion.admin.site.register(models.Facture, FactureAdmin)
eo_gestion.admin.site.register(models.Payment, PaymentAdmin)