213 lines
7.4 KiB
Python
213 lines
7.4 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
|
|
from decimal import Decimal
|
|
|
|
from django import forms
|
|
from django.contrib.admin import widgets
|
|
from django.core.exceptions import ValidationError
|
|
from django.db.transaction import atomic
|
|
|
|
from . import models
|
|
|
|
pourcentages = [(Decimal(i) / 100, "%s %%" % i) for i in range(0, 101, 5)]
|
|
|
|
|
|
class RapidFactureForm(forms.Form):
|
|
contrat = forms.ModelChoiceField(queryset=models.Contrat.objects.all())
|
|
pourcentage = forms.ChoiceField(choices=pourcentages, initial='1')
|
|
solde = forms.BooleanField(
|
|
required=False, label='Ignorer le pourcentage, et solder le contrat en une ligne.'
|
|
)
|
|
|
|
def __init__(self, request=None, *args, **kwargs):
|
|
self.request = request
|
|
super().__init__(*args, **kwargs)
|
|
|
|
@atomic
|
|
def clean(self):
|
|
contrat = self.cleaned_data['contrat']
|
|
pourcentage = Decimal(self.cleaned_data['pourcentage'])
|
|
solde = self.cleaned_data['solde']
|
|
facture = models.Facture(contrat=contrat, creator=self.request.user, taux_tva=contrat.tva)
|
|
try:
|
|
facture.clean()
|
|
except ValidationError as e:
|
|
raise forms.ValidationError(*e.args)
|
|
facture.save()
|
|
self.cleaned_data["facture"] = facture
|
|
lignes = []
|
|
errors = []
|
|
if solde:
|
|
montant_solde = contrat.solde()
|
|
if montant_solde == 0:
|
|
raise ValidationError('Le solde du contrat est déjà nul.')
|
|
models.Ligne.objects.create(
|
|
facture=facture, intitule='Solde', prix_unitaire_ht=montant_solde, quantite=Decimal(1)
|
|
)
|
|
else:
|
|
for prestation in contrat.prestations.all():
|
|
ligne = models.Ligne(
|
|
facture=facture,
|
|
intitule=prestation.intitule,
|
|
prix_unitaire_ht=prestation.prix_unitaire_ht,
|
|
quantite=prestation.quantite * pourcentage,
|
|
)
|
|
lignes.append(ligne)
|
|
try:
|
|
ligne.clean()
|
|
except ValidationError as e:
|
|
error = "Il y a un problème avec la ligne « %s »: " % prestation.intitule
|
|
error += "; ".join(map(lambda x: x.rstrip("."), e.messages))
|
|
errors.append(error)
|
|
if errors:
|
|
raise forms.ValidationError(errors)
|
|
|
|
for ligne in lignes:
|
|
ligne.save()
|
|
|
|
return self.cleaned_data
|
|
|
|
|
|
class DuplicateContractForm(forms.Form):
|
|
contrat = forms.ModelChoiceField(queryset=models.Contrat.objects.all())
|
|
new_intitule = forms.CharField(max_length=150, label="Nouvel intitulé")
|
|
|
|
def __init__(self, request=None, *args, **kwargs):
|
|
self.request = request
|
|
super().__init__(*args, **kwargs)
|
|
|
|
@atomic
|
|
def clean(self):
|
|
contrat = self.cleaned_data['contrat']
|
|
new_intitule = self.cleaned_data['new_intitule']
|
|
new_contrat = models.Contrat(
|
|
client=contrat.client,
|
|
intitule=new_intitule,
|
|
description=contrat.description,
|
|
tva=contrat.tva,
|
|
creator=self.request.user,
|
|
)
|
|
try:
|
|
new_contrat.clean()
|
|
except ValidationError as e:
|
|
raise forms.ValidationError(*e.args)
|
|
new_contrat.save()
|
|
self.cleaned_data["new_contrat"] = new_contrat
|
|
for prestation in contrat.prestations.all():
|
|
new_prestation = prestation.duplicate()
|
|
new_prestation.contrat = new_contrat
|
|
new_prestation.save()
|
|
return self.cleaned_data
|
|
|
|
|
|
class LigneForm(forms.ModelForm):
|
|
class Meta:
|
|
fields = '__all__'
|
|
model = models.Ligne
|
|
localized_fields = ("quantite", "prix_unitaire_ht", "taux_tva")
|
|
|
|
|
|
class PrestationForm(forms.ModelForm):
|
|
class Meta:
|
|
fields = '__all__'
|
|
model = models.Prestation
|
|
localized_fields = ("quantite", "prix_unitaire_ht")
|
|
|
|
|
|
class FactureForm(forms.ModelForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields['contrat'].queryset = models.Contrat.objects.none()
|
|
|
|
client_id = None
|
|
|
|
if 'client' in self.data:
|
|
try:
|
|
client_id = int(self.data.get('client'))
|
|
except (ValueError, TypeError):
|
|
pass # invalid input from client; ignore and fallback to empty Contrat queryset
|
|
elif 'client' in self.initial:
|
|
try:
|
|
client_id = int(self.initial.get('client'))
|
|
except (ValueError, TypeError):
|
|
pass # invalid input from client; ignore and fallback to empty Contrat queryset
|
|
|
|
if client_id:
|
|
self.fields['contrat'].queryset = models.Contrat.objects.filter(client_id=client_id)
|
|
|
|
def clean_proforma(self):
|
|
if not self.instance.proforma and 'proforma' in self.changed_data:
|
|
raise ValidationError('Une facture ne peut pas redevenir proforma.')
|
|
return self.cleaned_data['proforma']
|
|
|
|
def clean_emission(self):
|
|
if not self.instance.proforma and 'emission' in self.changed_data:
|
|
raise ValidationError('Seules les factures proforma peuvent changer de date d\'émission.')
|
|
return self.cleaned_data['emission']
|
|
|
|
def clean(self):
|
|
# réinitialiser la date d'émission quand une facture quitte le statut
|
|
# proforma, sauf si une date d'émission spécifique a été fixée à la
|
|
# main
|
|
cleaned_data = super().clean()
|
|
update_echeance = False
|
|
if self.instance.proforma and 'proforma' in self.changed_data and 'emission' not in self.changed_data:
|
|
cleaned_data['emission'] = models.today()
|
|
update_echeance = True
|
|
if (
|
|
cleaned_data.get('emission')
|
|
and 'emission' in self.changed_data
|
|
and 'echeance' not in self.changed_data
|
|
):
|
|
update_echeance = True
|
|
if update_echeance:
|
|
cleaned_data['echeance'] = cleaned_data['emission'] + datetime.timedelta(
|
|
days=models.DELAI_PAIEMENT
|
|
)
|
|
|
|
class Meta:
|
|
fields = '__all__'
|
|
model = models.Facture
|
|
localized_fields = ("taux_tva",)
|
|
|
|
|
|
class ClientForm(forms.ModelForm):
|
|
class Meta:
|
|
fields = '__all__'
|
|
model = models.Client
|
|
localized_fields = ("tva",)
|
|
widgets = {
|
|
"adresse": widgets.AdminTextareaWidget(attrs={"rows": 4}),
|
|
"contacts": widgets.AdminTextareaWidget(attrs={"rows": 4}),
|
|
}
|
|
|
|
|
|
class ContratForm(forms.ModelForm):
|
|
class Meta:
|
|
fields = '__all__'
|
|
model = models.Contrat
|
|
localized_fields = ("tva", "montant_sous_traite")
|
|
|
|
|
|
class PaymentForm(forms.ModelForm):
|
|
class Meta:
|
|
fields = '__all__'
|
|
model = models.Payment
|
|
localized_fields = ("montant_affecte",)
|