barbacompta/eo_gestion/eo_facture/templatetags/eo_facture.py

321 lines
11 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 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 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()
.filter(paid=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()
today_year = date.today().year
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 = sorted(
invoiced_by_year_and_client[year].keys(),
key=lambda x: -invoiced_by_year_and_client[year][x],
)
invoiced_clients_by_year[year] = clients
contracted_by_year = defaultdict(lambda: Decimal(0))
for contract in Contrat.objects.all().prefetch_related('factures', 'factures__lignes', 'prestations'):
adjust = 0
for year, fraction in contract.percentage_per_year:
amount = contract.montant() * fraction
if year > today_year:
contracted_by_year[year] += amount
else:
adjust += amount
for f in contract.factures.all():
if f.proforma:
continue
if f.accounting_year <= today_year:
adjust -= f.montant - f.sous_traite
if adjust > 0:
contracted_by_year[today_year] += adjust
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 = dict()
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)