# 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
import itertools
import os.path
from collections import defaultdict
from decimal import ROUND_HALF_UP, Decimal

from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator, validate_email
from django.db import models, transaction
from django.db.models import F, Q, Sum
from django.db.models.query import QuerySet
from django.db.models.signals import post_delete, post_save
from django.template.loader import get_template
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from weasyprint import HTML

from eo_gestion.utils import percentage_str

from ..eo_banque import models as banque_models
from . import facturx, fields, taggit, validators

User = get_user_model()

validate_telephone = RegexValidator(r'[. 0-9]*')

DEFAULT_TVA = getattr(settings, 'TVA', '20')


def accounting_rounding(amount):
    return amount.quantize(Decimal('0.01'), ROUND_HALF_UP)


def today():
    return now().date()


class Client(models.Model):
    nom = models.CharField(max_length=255, unique=True)
    notes_privees = models.TextField(verbose_name='Notes privées', blank=True)
    adresse = models.TextField()
    email = models.CharField(max_length=50, validators=[validate_email], verbose_name='Courriel', blank=True)
    telephone = models.CharField(
        max_length=20, validators=[validate_telephone], verbose_name='Téléphone', blank=True
    )
    contacts = models.TextField(verbose_name=_('Contacts'), blank=True)
    monnaie = models.CharField(max_length=10, default='€')
    tva = models.DecimalField(
        verbose_name='TVA par défaut', max_digits=4, decimal_places=2, default=Decimal(DEFAULT_TVA)
    )
    picture = models.ImageField('Logo', upload_to='logos/', blank=True, null=True)
    siret = models.CharField(
        max_length=len('29202001300010'),
        validators=[validators.validate_siret],
        db_index=True,
        blank=True,
        default='',
    )
    service_code = models.CharField(
        max_length=128,
        db_index=True,
        blank=True,
        default='',
    )
    active = models.BooleanField(verbose_name='Actif', default=True)

    chorus_structure = models.ForeignKey(
        'chorus.Structure',
        verbose_name='Structure ChorusPro',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )

    def __str__(self):  # pylint: disable=invalid-str-returned
        return self.nom

    def clean(self):
        self.service_code = self.service_code.strip()
        if self.chorus_structure:
            self.siret = self.chorus_structure.siret
            self.service_code = self.chorus_structure.service_code

    @classmethod
    @transaction.atomic
    def update_siret_and_service_code(cls):
        clients_to_update = (
            Client.objects.filter(Q(chorus_structure__isnull=False))
            .exclude(siret=F('chorus_structure__siret'), service_code=F('chorus_structure__service_code'))
            .annotate(
                new_siret=F('chorus_structure__siret'), new_service_code=F('chorus_structure__service_code')
            )
        )

        for client in clients_to_update:
            client.siret = client.new_siret
            client.service_code = client.new_service_code
            client.save(update_fields=['siret', 'service_code'])

    class Meta:
        ordering = ('nom',)


def one_hundred_percent_this_year():
    return '%s:100' % now().date().year


class Contrat(models.Model):
    client = models.ForeignKey(Client, related_name='contrats', on_delete=models.CASCADE, null=True)
    intitule = models.CharField(max_length=150)
    description = models.TextField(blank=True)
    public_description = models.TextField(
        verbose_name='Description publique',
        blank=True,
        help_text='Si elle est présente, cette description sera reprise dans la liste des références sur le site web.',
    )
    url = models.URLField(verbose_name=_('URL'), blank=True)
    tva = models.DecimalField(
        max_digits=4, decimal_places=2, default=Decimal(DEFAULT_TVA), blank=True, null=True
    )
    creation = models.DateField(default=today)
    creator = models.ForeignKey(User, verbose_name='Créateur', on_delete=models.CASCADE)
    percentage_per_year = fields.PercentagePerYearField(
        default=one_hundred_percent_this_year,
        verbose_name='Pourcentage par année',
        help_text='Incompatible avec la périodicité',
    )
    montant_sous_traite = models.DecimalField(max_digits=8, decimal_places=2, default=Decimal('0'))
    image = models.ImageField('Image', upload_to='images/', blank=True, null=True)
    numero_marche = models.CharField(max_length=128, verbose_name='Numéro du marché', blank=True, default='')

    periodicite = models.CharField(
        verbose_name='Périodicité',
        max_length=16,
        choices=[
            ('annuelle', 'Annuelle'),
            ('semestrielle', 'Semestrielle'),
        ],
        blank=True,
        null=True,
    )
    periodicite_debut = models.DateField(verbose_name='Périodicité début', blank=True, null=True)
    periodicite_fin = models.DateField(verbose_name='Périodicité fin', blank=True, null=True)

    tags = taggit.TaggableManager(blank=True)

    def montant_facture(self):
        return sum(facture.montant for facture in self.factures.non_proforma())

    def pourcentage_facture(self):
        return percentage_str(self.montant_facture(), self.montant())

    pourcentage_facture.short_description = 'Pourcentage facturé'

    @property
    def montant_prestations(self):
        return Decimal(sum(p.montant() for p in self.prestations.all()))

    def montant(self):
        """
        Montant total d'un contrat, y compris les prestations
        optionnelles. Si pas de prestation définie, montant des factures émises.
        """
        if self.prestations.exists():
            montant = self.montant_prestations
            if self.periodicite and self.periodicite_debut:
                return montant * self.periodicite_nombre_d_echeances()
            else:
                return montant

        else:
            return self.montant_facture()

    def solde(self):
        return self.montant() - self.montant_facture()

    def montant_par_annee(self):
        '''Calcul du revenu futur, en retranchant les
        montants déjà facturés par rapport au
        prévisionnel'''
        today_year = datetime.date.today().year
        montant_par_annee = defaultdict(Decimal)
        adjust = 0
        if not self.periodicite:
            for year, fraction in self.percentage_per_year:  # pylint: disable=not-an-iterable
                amount = self.montant() * fraction
                if year > today_year:
                    montant_par_annee[year] += amount
                else:
                    adjust += amount
        else:
            # calcule le montant par periode en sommant le montant des
            # prestations puis impute à chaque année ce même montant pour
            # chaque échéance arrivant dans cette année, pour les montants de
            # l'année en cours et des précédentes, tout mettre dans l'année
            # courante, ce sera diminué du total des factures déjà faites (même
            # comportement que dans le cas classique)
            montant = self.montant_prestations
            for _, debut, _ in self.periodicite_echeances(limit=20):
                if debut.year > today_year:
                    montant_par_annee[debut.year] += montant
                else:
                    adjust += montant
        for f in self.factures.all():
            if f.proforma:
                continue
            if f.accounting_year <= today_year:
                adjust -= f.montant - f.sous_traite
        if adjust > 0:
            montant_par_annee[today_year] += adjust
        return montant_par_annee

    def clean(self):
        self.numero_marche = self.numero_marche.strip()

        if self.periodicite and not self.periodicite_debut:
            raise ValidationError(
                {'periodicite_debut': 'Vous devez définir une date de début pour la période.'}
            )
        if self.periodicite_debut and not self.periodicite:
            raise ValidationError({'periodicite': 'Vous devez définir une périodicité.'})

        if self.periodicite_fin and not self.periodicite:
            raise ValidationError({'periodicite': 'Vous devez définir une périodicité.'})

        if self.periodicite and len(self.percentage_per_year) > 1:
            raise ValidationError(
                'Vous ne pouvez pas utiliser pourcentage par année en même temps que périodicité.'
            )

    @property
    def periodicite_duration(self):
        if self.periodicite == 'annuelle':
            return relativedelta(years=1)

        if self.periodicite == 'semestrielle':
            return relativedelta(months=6)

        raise ValueError

    def periodicite_dates(self):
        if self.periodicite == 'annuelle':
            durations = (relativedelta(years=i) for i in itertools.count())
        elif self.periodicite == 'semestrielle':
            durations = (relativedelta(months=6 * i) for i in itertools.count())
        else:
            raise ValueError('aucune échéance')
        dates = (self.periodicite_debut + duration for duration in durations)
        if self.periodicite_fin:
            return itertools.takewhile(lambda date: date < self.periodicite_fin, dates)
        else:
            return dates

    def periodicite_nombre_d_echeances(self):
        if self.periodicite_fin:
            return len(list(self.periodicite_echeances(limit=1000)))
        else:
            # compter les échéances passées + 1
            return len(list(self.periodicite_echeances(limit=1)))

    def periodicite_echeances(self, until=None, limit=3):
        i = 1
        count = 0
        n = now().date()
        for day in self.periodicite_dates():
            if until and until < day:
                break
            if self.periodicite_fin and day >= self.periodicite_fin:
                break
            periode_fin = day + self.periodicite_duration
            if day > n:
                count += 1
            yield i, day, periode_fin
            i += 1
            if count == limit and not self.periodicite_fin:
                break

    def save(self, *args, **kwargs):
        with transaction.atomic(savepoint=False):
            if not self.periodicite:
                # supprimer les échéances des factures si pas de periodicite
                self.factures.update(numero_d_echeance=None)
            return super().save(*args, **kwargs)

    def has_echeance_to_bill(self):
        if self.periodicite is None:
            return False

        until = (now() + relativedelta(months=6)).date()

        facture_par_numero_d_echeance = {
            facture.numero_d_echeance: facture
            for facture in self.factures.all()
            if facture.echeance and not facture.proforma
        }
        for i, _, __ in self.periodicite_echeances(until=until):
            if i not in facture_par_numero_d_echeance:
                return True
        return False

    def next_echeance_to_bill(self):
        until = (now() + relativedelta(months=6)).date()
        echeances = []

        facture_par_numero_d_echeance = {
            facture.numero_d_echeance: facture
            for facture in self.factures.all()
            if facture.echeance and not facture.proforma
        }
        for i, periode_debut, periode_fin in self.periodicite_echeances(until=until):
            if i in facture_par_numero_d_echeance:
                continue
            echeances.append((periode_debut, periode_fin, i))
        echeances.sort()
        if len(echeances) > 0:
            return echeances[0][2]
        return -1

    def __str__(self):  # pylint: disable=invalid-str-returned
        return self.intitule

    class Meta:
        ordering = ('-id',)


class Prestation(models.Model):
    contrat = models.ForeignKey(Contrat, related_name='prestations', on_delete=models.CASCADE)
    intitule = models.CharField(max_length=255, verbose_name='Intitulé')
    optionnel = models.BooleanField(blank=True, default=False)
    prix_unitaire_ht = models.DecimalField(max_digits=8, decimal_places=2)
    quantite = models.DecimalField(max_digits=8, decimal_places=3)

    def montant(self):
        return accounting_rounding(self.prix_unitaire_ht * self.quantite)

    def duplicate(self):
        return Prestation(
            contrat=self.contrat,
            intitule=self.intitule,
            optionnel=self.optionnel,
            prix_unitaire_ht=self.prix_unitaire_ht,
            quantite=self.quantite,
        )

    def __str__(self):
        return '%s pour %5.2f € HT' % (
            self.intitule,
            accounting_rounding(self.prix_unitaire_ht * self.quantite),
        )

    class Meta:
        ordering = ('contrat', 'id')


DELAI_PAIEMENT = getattr(settings, 'DELAI_PAIEMENT', 45)


def today_plus_delai():
    return today() + datetime.timedelta(days=DELAI_PAIEMENT)


echeance_verbose_name = 'Échéance (par défaut %d jours)' % getattr(settings, 'DELAI_PAIEMENT', 45)


class FactureQuerySet(QuerySet):
    def avec_solde(self):
        return self.non_proforma().filter(paid=False)

    def non_proforma(self):
        return self.filter(proforma=False)

    def for_year(self, year):
        return self.filter(
            Q(emission__year=year, account_on_previous_period=False)
            | Q(emission__year=year - 1, account_on_previous_period=True)
        )


class Facture(models.Model):
    proforma = models.BooleanField(default=True, verbose_name='Devis', db_index=True)
    ordre = models.IntegerField(
        null=True,
        blank=True,
        verbose_name='Numéro de la facture dans l\'année',
        unique_for_year='emission',
        editable=False,
    )
    client = models.ForeignKey(
        Client, related_name='direct_factures', null=True, blank=True, on_delete=models.CASCADE
    )
    contrat = models.ForeignKey(
        Contrat, related_name='factures', blank=True, null=True, on_delete=models.CASCADE
    )
    annulation = models.ForeignKey(
        'Facture', related_name='factures_avoir', blank=True, null=True, on_delete=models.CASCADE
    )
    intitule = models.CharField(max_length=150, verbose_name='Intitulé', blank=True)
    notes = models.TextField(blank=True)
    emission = models.DateField(verbose_name='Émission', default=today, db_index=True)
    echeance = models.DateField(verbose_name=echeance_verbose_name, default=today_plus_delai)
    taux_tva = models.DecimalField(
        max_digits=4, decimal_places=2, default=Decimal(DEFAULT_TVA), blank=True, null=True
    )
    sous_traite = fields.EuroField(
        default=Decimal(0),
        verbose_name='Montant sous-traité',
        help_text='indiquer une somme pas un pourcentage, hors-taxe',
    )
    paid = models.BooleanField(blank=True, verbose_name='Soldée', default=False, db_index=True)
    creator = models.ForeignKey(User, verbose_name='Créateur', on_delete=models.CASCADE)
    account_on_previous_period = models.BooleanField(
        verbose_name='Mettre cette facture sur l\'exercice précédent', default=False
    )
    numero_engagement = models.CharField(
        max_length=128, verbose_name='Numéro d\'engagement', blank=True, default=''
    )
    private_notes = models.TextField(
        blank=True,
        verbose_name='Notes privées',
        help_text='À usage purement interne, ne seront jamais présentes sur la facture',
    )
    numero_d_echeance = models.IntegerField(
        verbose_name='Numéro d\'échéance',
        blank=True,
        null=True,
    )

    objects = FactureQuerySet.as_manager()

    def last_ordre_plus_one(self):
        ordre_max = Facture.objects.filter(emission__year=self.emission.year).aggregate(models.Max('ordre'))[
            'ordre__max'
        ]
        return (ordre_max or 0) + 1

    def code(self):
        if not self.ordre and self.proforma:
            return f'Devis n°{self.id} du {self.emission}'
        ctx = {}
        ctx.update(self.__dict__)
        ctx.update({'year': self.emission.year})
        if ctx['ordre'] is None:
            return 'Ordre is missing'
        format = getattr(settings, 'FACTURE_CODE_FORMAT', 'F%(year)s%(ordre)04d')
        return format % ctx

    @property
    def pk_or_code(self):
        # used in ModelAdmin to create URLs for humans
        if self.ordre is not None:
            return self.code()
        return self.pk

    def save(self, *args, **kwargs):
        if self.ordre is None and not self.proforma:
            self.ordre = self.last_ordre_plus_one()
        super().save(*args, **kwargs)

    def clean(self):
        if not (self.contrat or self.client):
            raise ValidationError('La facture doit avoir un client ou un contrat')
        if self.contrat:
            if not self.intitule:
                self.intitule = self.contrat.intitule
            if self.client:
                if self.client != self.contrat.client and self.contrat.client is not None:
                    raise ValidationError('Le client de la facture et du contrat doivent être identiques.')
            else:
                self.client = self.contrat.client
            if self.contrat.periodicite and not self.annulation:
                if self.numero_d_echeance is None:
                    raise ValidationError(
                        {
                            'numero_d_echeance': 'Vous devez définir un numéro d\'échéance car le contrat est périodique.'
                        }
                    )
                for i, dummy, dummy in self.contrat.periodicite_echeances():
                    if i == self.numero_d_echeance:
                        break
                else:
                    raise ValidationError('Numéro d\'échéance invalide')
            else:
                if self.numero_d_echeance is not None:
                    raise ValidationError(
                        'Un numéro d\'échéance ne peut être défini que pour un contrat récurrent.'
                    )
        else:
            if not self.intitule:
                raise ValidationError('La facture doit avoir un intitulé')
            if self.numero_d_echeance is not None:
                raise ValidationError(
                    'Un numéro d\'échéance ne peut être défini que pour un contrat récurrent.'
                )
        if not self.proforma:
            try:
                for ligne in self.lignes.all():
                    ligne.clean()
            except ValidationError:
                raise ValidationError('Il y a un problème avec les lignes de cette facture')
        self.update_paid(save=False)
        self.numero_engagement = self.numero_engagement.strip()
        if (
            not self.proforma
            and self.client
            and self.client.chorus_structure
            and self.client.chorus_structure.engagement_obligatoire
        ):
            if not self.numero_engagement:
                raise ValidationError('Numéro d\'engagement obligatoire pour Chorus')

    def index(self):
        if self.contrat:
            return list(self.contrat.factures.order_by('emission')).index(self) + 1
        else:
            return 1

    def __str__(self):
        return self.code()

    @property
    def tva(self):
        amount_by_vat = defaultdict(lambda: Decimal(0))
        for ligne in self.lignes.all():
            amount_by_vat[ligne.tva] += ligne.montant
        vat = Decimal(0)
        for vat_percentage, amount in amount_by_vat.items():
            var_ratio = vat_percentage / Decimal('100')
            vat += accounting_rounding(var_ratio * amount)
        return vat

    @property
    def montant_ttc(self):
        return self.montant + self.tva

    @property
    def montant(self):
        return sum(ligne.montant for ligne in self.lignes.all())

    def solde(self):
        payments = self.payments.all()
        if payments.count():
            amount_paid = sum(payment.montant_affecte for payment in payments)
        else:
            amount_paid = Decimal(0)
        return amount_paid - self.montant_ttc

    def update_paid(self, save=True):
        new_paid = self.solde() >= 0
        if self.montant > 0 and new_paid and self.paid != new_paid:
            self.paid = new_paid
            if save:
                self.save()

    @property
    def accounting_year(self):
        if self.account_on_previous_period:
            return self.emission.year - 1
        return self.emission.year

    DEFAULT_FACTURE_TEMPLATE = 'facture.html'

    def html(self, template_name=None, base_uri=None):
        template = get_template(template_name or self.DEFAULT_FACTURE_TEMPLATE)
        return template.render({'facture': self, 'base_uri': base_uri or ''})

    def pdf(self, template_name=None, base_uri=None):
        html = HTML(string=self.html(template_name=template_name, base_uri=base_uri))
        pdf = html.write_pdf()

        if hasattr(settings, 'FACTURE_DIR'):
            filename = os.path.join(settings.FACTURE_DIR, self.filename_with_client())
            with open(filename, 'wb') as fd:
                fd.write(pdf)
        return pdf

    def facturx_pdf(self, template_name=None, base_uri=None):
        if self.proforma:
            raise RuntimeError('facturx_pdf() ne peut être appelé que sur des les factures, pas les devis')
        facturx_ctx = {
            'numero_de_facture': self.code(),
            'type_facture': '380' if not self.annulation else '381',  # 380 => facture, 381 => avoir
            'date_emission': self.emission,
            'vendeur': settings.VENDOR_NAME,
            'vendeur_siret': settings.VENDOR_SIRET,
            'vendeur_tvai': settings.VENDOR_TVAI,
            'client_siret': self.client.siret,
            'client_name': self.client.nom,
            'chorus_service_code': self.client.service_code,
            'numero_engagement': self.numero_engagement,
            'numero_marche': (self.contrat and self.contrat.numero_marche) or '',
            'montant_ht': str(self.montant),
            'montant_ttc': str(self.montant_ttc),
            'montant_tva': str(self.tva),
            'taux_tva': self.taux_tva or (self.contrat and self.contrat.tva) or self.client.tva or 0,
            'annulation_code': self.annulation.code() if self.annulation else None,
        }
        return facturx.add_facturx_from_bytes(
            self.pdf(template_name=template_name, base_uri=base_uri), facturx_ctx
        )

    def cancel(self, creator):
        assert not self.annulation, 'cannot cancel a canceled invoice'
        assert not self.factures_avoir.count(), 'cannot cancel twice an invoice'

        facture_avoir = Facture.objects.get(pk=self.pk)
        facture_avoir.pk = None
        facture_avoir.ordre = None
        facture_avoir.annulation = self
        facture_avoir.proforma = True
        facture_avoir.intitule = 'AVOIR POUR LA FACTURE ' + self.intitule
        facture_avoir.paid = False
        facture_avoir.creator = creator
        facture_avoir.account_on_previous_period = False
        facture_avoir.numero_d_echeance = None
        facture_avoir.save()
        for ligne in self.lignes.all():
            ligne.pk = None
            ligne.facture = facture_avoir
            ligne.quantite = ligne.quantite * -1
            ligne.save()
        for payment in self.payments.all():
            payment.pk = None
            payment.save()
        return facture_avoir

    def filename(self):
        avoir = '-AVOIR' if self.annulation else ''
        filename = f'{self.code()}{avoir}.pdf'
        return filename

    def filename_with_client(self):
        avoir = '-AVOIR' if self.annulation else ''
        filename = f'{self.code()}-{self.client.nom}{avoir}.pdf'
        return filename

    @transaction.atomic(savepoint=False)
    def import_ligne(self):
        for prestation in self.contrat.prestations.all():
            ligne, _ = Ligne.objects.update_or_create(
                facture=self,
                intitule=prestation.intitule,
                defaults={
                    'prix_unitaire_ht': prestation.prix_unitaire_ht,
                    'quantite': prestation.quantite,
                },
            )
            ligne.clean()

    def periode(self):
        if self.contrat and self.contrat.periodicite and self.numero_d_echeance is not None:
            for i, debut, fin in self.contrat.periodicite_echeances():
                if i == self.numero_d_echeance:
                    debut = debut.strftime('%d/%m/%Y')
                    fin = fin.strftime('%d/%m/%Y')
                    return f'du {debut} au {fin}'

    periode.short_description = 'Dates de l\'échéance'

    periode = property(periode)

    class Meta:
        ordering = ('-id',)
        unique_together = [
            ('contrat', 'numero_d_echeance'),
        ]


class Ligne(models.Model):
    facture = models.ForeignKey(Facture, related_name='lignes', on_delete=models.CASCADE)
    intitule = models.TextField(blank=True)
    prix_unitaire_ht = models.DecimalField(max_digits=8, decimal_places=2, default=Decimal('0'))
    quantite = models.DecimalField(max_digits=8, decimal_places=3, default=Decimal('1.0'))
    taux_tva = models.DecimalField(max_digits=4, decimal_places=2, blank=True, null=True)
    order = models.IntegerField(blank=True, null=True, default=0)

    @property
    def montant(self):
        return accounting_rounding(self.prix_unitaire_ht * self.quantite)

    @property
    def tva(self):
        if self.taux_tva is not None:
            return self.taux_tva
        elif self.facture.taux_tva is not None:
            return self.facture.taux_tva
        elif self.facture.contrat and self.facture.contrat.tva is not None:
            return self.facture.contrat.tva
        else:
            return self.facture.client.tva

    def clean(self):
        errors = []
        if self.taux_tva and self.taux_tva < 0:
            errors.append('Le taux de tva doit être une valeur positive ou nulle.')
        if self.prix_unitaire_ht < 0:
            errors.append('Le prix unitaire hors taxe doit être une valeur positive ou nulle.')
        if self.facture.contrat and not self.facture.proforma:
            facture = self.facture
            contrat = facture.contrat
            deja_facture = sum(f.montant for f in contrat.factures.exclude(id=facture.id))
            deja_facture += sum(l.montant for l in facture.lignes.all() if l != self)
            if deja_facture + self.montant > contrat.montant():
                errors.append(
                    'Cette ligne fait dépasser le montant initial du contrat de %.2f %s'
                    % (deja_facture + self.montant - contrat.montant(), contrat.client.monnaie)
                )
        if errors:
            raise ValidationError(errors)

    class Meta:
        ordering = ('order',)

    def __str__(self):
        if self.facture.client:
            return '%s pour %s %s' % (
                self.intitule,
                self.montant,
                self.facture.client.monnaie,
            )
        else:
            return '%s pour %s €' % (self.intitule, self.montant)


def encaissements_avec_solde_non_affecte():
    query = Q(montant_affecte__lt=F('montant')) | Q(montant_affecte__isnull=True)
    return (
        banque_models.LigneBanquePop.objects.filter(montant__gt=0)
        .annotate(montant_affecte=Sum('payments__montant_affecte'))
        .filter(query)
        .order_by('-date_valeur')
    )


class Payment(models.Model):
    facture = models.ForeignKey(Facture, related_name='payments', on_delete=models.CASCADE)
    ligne_banque_pop = models.ForeignKey(
        banque_models.LigneBanquePop,
        related_name='payments',
        verbose_name='Encaissement',
        limit_choices_to={'montant__gt': 0},
        on_delete=models.CASCADE,
    )
    montant_affecte = models.DecimalField(
        max_digits=8,
        decimal_places=2,
        blank=True,
        verbose_name='Montant affecté',
        help_text="Si vide, le montant non affecté de l'encaissement est pris comme valeur",
    )

    def clean(self):
        """Vérifie la cohérence des paiements"""
        try:
            if self.montant_affecte is None:
                self.montant_affecte = min(self.ligne_banque_pop.montant, self.facture.montant_ttc)
        except (banque_models.LigneBanquePop.DoesNotExist, Facture.DoesNotExist):
            pass
        aggregate = models.Sum('montant_affecte')
        # from the ligne de banque pov
        try:
            other_payments = self.ligne_banque_pop.payments
            if self.ligne_banque_pop.montant < 0 or (self.montant_affecte and self.montant_affecte <= 0):
                raise ValidationError("Un paiement ne peut être que d'un montant positif")
        except banque_models.LigneBanquePop.DoesNotExist:
            pass
        else:
            deja_affecte = other_payments.aggregate(aggregate).get('montant_affecte', 0)
            if deja_affecte + self.montant_affecte > self.ligne_banque_pop.montant:
                raise ValidationError(
                    'Le montant affecté aux différentes factures '
                    'est supérieur au montant de l\'encaissement.'
                )
        # from the facture pov
        try:
            other_payments = self.facture.payments
        except Facture.DoesNotExist:
            pass
        else:
            deja_affecte = other_payments.aggregate(aggregate).get('montant_affecte') or 0
            if (
                self.montant_affecte is not None
                and deja_affecte + self.montant_affecte > self.facture.montant_ttc
            ):
                raise ValidationError(
                    'Le montant affecté aux différentes factures '
                    'est supérieur au montant de l\'encaissement.'
                )

    def __str__(self):
        return 'Paiement de %.2f € sur facture %s à %s' % (
            self.ligne_banque_pop.montant,
            self.facture,
            self.facture.client,
        )

    class Meta:
        verbose_name = 'Paiement'
        ordering = ('-ligne_banque_pop__date_valeur',)


def update_paid(sender, instance, raw=False, **kwargs):
    if raw:
        return
    instance.facture.update_paid()


post_save.connect(update_paid, sender=Payment)
post_delete.connect(update_paid, sender=Payment)
post_save.connect(update_paid, sender=Ligne)
post_delete.connect(update_paid, sender=Ligne)
