barbacompta/eo_gestion/eo_facture/templatetags/eo_facture.py

347 lines
12 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/>.
from collections import defaultdict
from datetime import date, datetime, timedelta
from decimal import Decimal, InvalidOperation
from dateutil.relativedelta import relativedelta
from django import template
from django.db import transaction
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils.formats import number_format
from django.utils.six import text_type
from django.utils.timesince import timesince
from django.utils.timezone import now
from eo_gestion.eo_banque.models import LigneBanquePop
from eo_gestion.eo_facture.models import DELAI_PAIEMENT, Contrat, Facture, Payment
from eo_gestion.utils import percentage
from ...decorators import cache
register = template.Library()
@register.inclusion_tag('eo_facture/impayee.html')
@cache
def impayees():
factures_impayees = [
f
for f in Facture.objects.select_related('client', 'contrat', 'contrat__client')
.prefetch_related('lignes', 'payments')
.avec_solde()
.exclude(paid=True)
.exclude(annulation__isnull=False)
.exclude(factures_avoir__isnull=False)
.order_by('emission')
if f.solde() < 0
]
factures = []
soldes = [-f.solde() for f in factures_impayees]
soldes_ht = [
f.montant * (solde / f.montant_ttc) if f.montant_ttc else Decimal(0)
for solde, f in zip(soldes, factures_impayees)
]
solde = sum(soldes)
solde_ht = sum(soldes_ht)
today = date.today()
after = datetime.now() - timedelta(days=365)
after = after.date()
lignes = LigneBanquePop.objects.filter(date_operation__gte=after, montant__in=soldes, payments=None)
for f in factures_impayees:
vieille = False
if (today - f.emission) > timedelta(days=DELAI_PAIEMENT):
vieille = True
encaissements = [ligne for ligne in lignes if ligne.montant == f.montant_ttc]
factures.append({'facture': f, 'encaissements': encaissements, 'vieille': vieille})
return {'montant': solde, 'montant_ht': solde_ht, 'factures': factures}
@register.inclusion_tag('eo_facture/income.html')
@cache
def income():
def AmountByYear():
return defaultdict(lambda: Decimal(0))
invoices = (
Facture.objects.filter(proforma=False)
.select_related('client', 'contrat', 'contrat__client')
.prefetch_related('lignes', 'payments')
)
paid_by_year = AmountByYear()
sous_traite_by_year = AmountByYear()
for invoice in invoices:
if invoice.paid:
paid_by_year[invoice.accounting_year] += invoice.montant
else:
tva = invoice.tva
ht = invoice.montant
try:
taux_moyen = tva / ht
except (ZeroDivisionError, InvalidOperation):
taux_moyen = Decimal(0)
paid_by_year[invoice.accounting_year] += (
(invoice.montant_ttc + invoice.solde()) * Decimal(1) / (Decimal(1) + taux_moyen)
)
invoiced_by_year = defaultdict(lambda: Decimal(0))
invoiced_by_year_and_client = defaultdict(lambda: defaultdict(lambda: Decimal(0)))
for invoice in invoices:
montant = invoice.montant - invoice.sous_traite
invoiced_by_year[invoice.accounting_year] += montant
sous_traite_by_year[invoice.accounting_year] += invoice.sous_traite
invoiced_by_year_and_client[invoice.accounting_year][invoice.client.id] += montant
invoiced_clients_by_year = {}
for year in invoiced_by_year_and_client:
clients = list(
sorted(
invoiced_by_year_and_client[year].keys(),
key=lambda x, year=year: -invoiced_by_year_and_client[year][x],
)
)
invoiced_clients_by_year[year] = clients
# compute future income for each contract
contracted_by_year = defaultdict(lambda: Decimal(0))
for contract in Contrat.objects.all().prefetch_related('factures', 'factures__lignes', 'prestations'):
for year, montant in contract.montant_par_annee().items():
contracted_by_year[year] += montant
this_year = date.today().year
income_by_year = []
for year in invoiced_by_year:
invoiced_by_year[year] -= paid_by_year[year]
for year in range(this_year - 1, this_year + 2):
income_by_year.append(
{
'year': year,
'paid': paid_by_year[year],
'invoiced': invoiced_by_year[year],
'contracted': contracted_by_year[year],
'sous_traite': sous_traite_by_year[year],
'total': paid_by_year[year] + invoiced_by_year[year] + contracted_by_year[year],
}
)
return {'income_by_year': income_by_year}
def zero_dict():
return defaultdict(lambda: Decimal(0))
class StringWithHref(text_type):
def __new__(cls, v, href=None):
return text_type.__new__(cls, v)
def __init__(self, v, href=None):
super().__init__()
self.href = href
def client_and_link(c):
s = text_type(c)
url = reverse("admin:eo_facture_client_change", args=(c.id,))
return StringWithHref(s, url)
def dict_of_list():
return defaultdict(lambda: [])
@register.inclusion_tag("eo_facture/table.html")
@cache
def income_by_clients(year=None):
if not year:
year = date.today().year
contracted_by_clients = zero_dict()
invoiced_by_clients = zero_dict()
total_by_clients = zero_dict()
contracts_by_clients = dict_of_list()
invoices_by_clients = dict_of_list()
for contrat in Contrat.objects.select_related('client').prefetch_related(
'factures', 'factures__lignes', 'prestations'
):
# how much as already been invoiced before previsions
adjust = 0
invoiced = 0
for percent_year, fraction in contrat.percentage_per_year:
if percent_year > year:
continue
adjust += contrat.montant() * fraction
for facture in contrat.factures.all():
if facture.proforma:
continue
if facture.accounting_year <= year:
adjust -= facture.montant
if facture.accounting_year == year:
invoiced += facture.montant
if adjust > 0:
contracted_by_clients[contrat.client] += adjust
total_by_clients[contrat.client] += adjust
if invoiced:
invoiced_by_clients[contrat.client] += invoiced
total_by_clients[contrat.client] += invoiced
for facture in (
Facture.objects.select_related('client')
.filter(contrat__isnull=True)
.for_year(year)
.prefetch_related('lignes')
):
if facture.proforma:
continue
invoiced_by_clients[facture.client] += facture.montant
total_by_clients[facture.client] += facture.montant
total = sum(total_by_clients.values())
total_invoiced = sum(invoiced_by_clients.values())
total_contracted = sum(contracted_by_clients.values())
percent_by_clients = {i: Decimal(100) * v / total for i, v in total_by_clients.items()}
clients = sorted(total_by_clients.keys(), key=lambda x: -total_by_clients[x])
running_total = Decimal(0)
# compute pareto index
pareto = {}
for client in clients:
running_total += percent_by_clients[client]
pareto[client] = running_total
return dict(
title="Chiffre d'affaire prévisionnel par client pour %s" % year,
name="previsional-income-by-client-%s" % year,
headers=[
('client', 'Client'),
('income', 'Chiffre d\'affaire'),
('invoiced', 'Facturé'),
('percent_invoiced', 'Pourcentage facturé'),
('contracted', 'Contracté'),
('percent_contracted', 'Pourcentage contracté'),
('percent', 'Pourcentage'),
('pareto', 'Pareto'),
],
contracts_by_clients=list(contracts_by_clients.items()),
invoices_by_clients=list(invoices_by_clients.items()),
table=[
(
client_and_link(c),
total_by_clients[c],
invoiced_by_clients[c],
percentage(invoiced_by_clients[c], total_by_clients[c]),
contracted_by_clients[c],
percentage(contracted_by_clients[c], total_by_clients[c]),
percent_by_clients[c],
pareto[c],
)
for c in clients
]
+ [
(
'Total',
total,
total_invoiced,
percentage(total_invoiced, total),
total_contracted,
percentage(total_contracted, total),
Decimal(100),
'',
)
],
)
@register.inclusion_tag('eo_facture/a_facturer.html')
@cache
def a_facturer():
contrats_a_facturer = []
for contrat in (
Contrat.objects.all()
.select_related('client')
.prefetch_related('prestations', 'factures', 'factures__lignes')
):
facture = contrat.montant_facture()
a_facture = contrat.montant()
if a_facture and facture < a_facture:
if contrat.factures.count() > 0:
depuis = max(facture.emission for facture in contrat.factures.all())
else:
depuis = contrat.creation
depuis = (date.today() - depuis).days
contrats_a_facturer.append(
{
'contrat': contrat,
'pourcentage': (facture / a_facture) * Decimal(100),
'montant': a_facture - facture,
'depuis': depuis,
}
)
contrats_a_facturer.sort(key=lambda x: -x['depuis'])
montant = sum(x['montant'] for x in contrats_a_facturer)
return {'a_facturer': contrats_a_facturer, 'montant': montant}
@register.filter(name='ago')
def ago(date):
ago = timesince(date)
# selects only the first part of the returned string
return ago.split(",")[0]
@register.filter(is_safe=True)
def amountformat(value, use_l10n=True):
return number_format(Decimal(value).quantize(Decimal("0.01")), force_grouping=True)
# invalidate impayees() cache when Payment set changes
@receiver(post_save, sender=Payment)
def payment_post_save(raw, **kwargs):
if raw:
return
transaction.on_commit(impayees.recompute)
@receiver(post_delete, sender=Payment)
def payment_post_delete(**kwargs):
transaction.on_commit(impayees.recompute)
@register.inclusion_tag('eo_facture/echeances.html')
def echeances():
qs = Contrat.objects.filter(periodicite__isnull=False).prefetch_related('factures')
until = (now() + relativedelta(months=6)).date()
echeances = []
for contrat in qs:
facture_par_numero_d_echeance = {
facture.numero_d_echeance: facture
for facture in contrat.factures.all()
if facture.echeance and not facture.proforma
}
for i, periode_debut, periode_fin in contrat.periodicite_echeances(until=until):
if i in facture_par_numero_d_echeance:
continue
echeances.append((periode_debut, periode_fin, i, contrat))
echeances.sort(key=lambda x: x[:3])
return {
'echeances': [
{
'debut': echeance[0],
'fin': echeance[1],
'occurrence': echeance[2],
'contrat': echeance[3],
}
for echeance in echeances
],
}