facture: add invoicing periodicity on contracts (#326)
This commit is contained in:
parent
b1f0842647
commit
91679603b1
|
@ -24,11 +24,12 @@ 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 import transaction
|
||||
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.urls import path, reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.six import BytesIO
|
||||
|
||||
|
@ -224,12 +225,66 @@ class ContratAdmin(LookupAllowed, admin.ModelAdmin):
|
|||
|
||||
return render(request, "admin/eo_facture/contrat/duplicate.html", context=context)
|
||||
|
||||
def facturer_echeance(self, request, object_id):
|
||||
if request.method != 'POST':
|
||||
raise http.Http404
|
||||
|
||||
contrat = self.get_object(request, object_id)
|
||||
|
||||
assert contrat.periodicite
|
||||
|
||||
echeance = int(request.POST['echeance'])
|
||||
|
||||
numero = None
|
||||
for i, debut, _ in contrat.periodicite_echeances():
|
||||
if i == echeance:
|
||||
numero = i
|
||||
break
|
||||
|
||||
assert numero is not None
|
||||
|
||||
with transaction.atomic():
|
||||
facture, _ = models.Facture.objects.update_or_create(
|
||||
client=contrat.client,
|
||||
contrat=contrat,
|
||||
echeance=debut,
|
||||
numero_d_echeance=numero,
|
||||
defaults={
|
||||
'creator': request.user,
|
||||
'numero_d_echeance': numero,
|
||||
},
|
||||
)
|
||||
if facture.proforma:
|
||||
facture.intitule = f'{contrat.intitule} {facture.periode}'
|
||||
facture.clean()
|
||||
facture.save()
|
||||
facture.import_ligne()
|
||||
|
||||
return http.HttpResponseRedirect(reverse('admin:eo_facture_facture_change', args=(facture.id,)))
|
||||
|
||||
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')]
|
||||
facturer_echeance_view = self.admin_site.admin_view(self.facturer_echeance)
|
||||
my_urls = [
|
||||
url(r'^(.+)/duplicate/$', duplicate_view, name='eo_facture_contrat_duplicate'),
|
||||
path(
|
||||
'<path:object_id>/facturer-echeance/',
|
||||
facturer_echeance_view,
|
||||
name='eo_facture_contrat_facturer_echeance',
|
||||
),
|
||||
]
|
||||
return my_urls + urls
|
||||
|
||||
def get_fields(self, request, obj=None):
|
||||
fields = list(super().get_fields(request, obj=obj))
|
||||
if obj:
|
||||
if obj.periodicite:
|
||||
fields = [field for field in fields if field != 'percentage_per_year']
|
||||
elif obj.percentage_per_year and len(obj.percentage_per_year) > 1:
|
||||
fields = [field for field in fields if field != 'periodicite']
|
||||
return fields
|
||||
|
||||
|
||||
def index(facture):
|
||||
return format_html('{0}', ordinal(facture.index()))
|
||||
|
@ -272,7 +327,7 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
|
|||
'montant_ttc',
|
||||
'account_on_previous_period',
|
||||
]
|
||||
readonly_fields = ['creator', 'solde', 'ordre', 'montant', 'montant_ttc']
|
||||
readonly_fields = ['creator', 'solde', 'ordre', 'montant', 'montant_ttc', 'periode']
|
||||
date_hierarchy = 'emission'
|
||||
list_select_related = True
|
||||
save_on_top = True
|
||||
|
@ -296,6 +351,12 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
|
|||
|
||||
column_solde.short_description = 'Solde'
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
# ne pas supprimer les factures émises
|
||||
if obj and not obj.proforma:
|
||||
return False
|
||||
return super().has_delete_permission(request, obj=obj)
|
||||
|
||||
def get_queryset(self, request):
|
||||
from django.db import connection
|
||||
|
||||
|
@ -396,8 +457,9 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
|
|||
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)
|
||||
if obj.client:
|
||||
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'
|
||||
|
||||
|
@ -431,6 +493,18 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
|
|||
changelist.pk_attname = 'pk_or_code'
|
||||
return changelist
|
||||
|
||||
def get_fields(self, request, obj=None):
|
||||
fields = list(super().get_fields(request, obj=obj))
|
||||
if not obj or not obj.contrat or obj.contrat.periodicite:
|
||||
fields += ['numero_d_echeance', 'periode']
|
||||
return fields
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = list(super().get_readonly_fields(request, obj=obj))
|
||||
if obj and obj.contrat and obj.contrat.periodicite:
|
||||
fields += ['periode']
|
||||
return fields
|
||||
|
||||
|
||||
class PaymentAdmin(LookupAllowed, admin.ModelAdmin, CommonPaymentInline):
|
||||
form = forms.PaymentForm
|
||||
|
|
|
@ -165,18 +165,19 @@ class FactureForm(forms.ModelForm):
|
|||
# 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:
|
||||
self.cleaned_data['emission'] = models.today()
|
||||
cleaned_data['emission'] = models.today()
|
||||
update_echeance = True
|
||||
if (
|
||||
self.cleaned_data.get('emission')
|
||||
cleaned_data.get('emission')
|
||||
and 'emission' in self.changed_data
|
||||
and 'echeance' not in self.changed_data
|
||||
):
|
||||
update_echeance = True
|
||||
if update_echeance:
|
||||
self.cleaned_data['echeance'] = self.cleaned_data['emission'] + datetime.timedelta(
|
||||
cleaned_data['echeance'] = cleaned_data['emission'] + datetime.timedelta(
|
||||
days=models.DELAI_PAIEMENT
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# Generated by Django 2.2.24 on 2021-11-20 17:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('eo_facture', '0014_facture_annulation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contrat',
|
||||
name='periodicite',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[('annuelle', 'Annuelle'), ('semestrielle', 'Semestrielle')],
|
||||
max_length=16,
|
||||
null=True,
|
||||
verbose_name='Périodicité',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contrat',
|
||||
name='periodicite_debut',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Périodicité début'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contrat',
|
||||
name='periodicite_fin',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Périodicité fin'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='facture',
|
||||
name='numero_d_echeance',
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name="Numéro d'échéance"),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='facture',
|
||||
unique_together={('contrat', 'numero_d_echeance')},
|
||||
),
|
||||
]
|
|
@ -16,15 +16,17 @@
|
|||
|
||||
|
||||
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
|
||||
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
|
||||
|
@ -119,11 +121,26 @@ class Contrat(models.Model):
|
|||
tva = models.DecimalField(max_digits=8, decimal_places=2, default=Decimal(DEFAULT_TVA))
|
||||
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)
|
||||
percentage_per_year = fields.PercentagePerYearField(
|
||||
default=one_hundred_percent_this_year, verbose_name='Pourcentage par année'
|
||||
)
|
||||
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):
|
||||
|
@ -156,6 +173,73 @@ class Contrat(models.Model):
|
|||
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 not self.periodicite_fin:
|
||||
return '*'
|
||||
return len(list(self.periodicite_echeances(limit=1000)))
|
||||
|
||||
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
|
||||
periode_fin = day + self.periodicite_duration
|
||||
if self.periodicite_fin and periode_fin > self.periodicite_fin:
|
||||
break
|
||||
if day > n:
|
||||
count += 1
|
||||
yield i, day, periode_fin
|
||||
i += 1
|
||||
if count == limit:
|
||||
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 __str__(self): # pylint: disable=invalid-str-returned
|
||||
return self.intitule
|
||||
|
||||
|
@ -257,6 +341,11 @@ class Facture(models.Model):
|
|||
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()
|
||||
|
||||
|
@ -300,9 +389,30 @@ class Facture(models.Model):
|
|||
raise ValidationError("Le client de la facture et du contrat doivent être identiques.")
|
||||
else:
|
||||
self.client = self.contrat.client
|
||||
if self.contrat.periodicite:
|
||||
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():
|
||||
|
@ -444,8 +554,36 @@ class Facture(models.Model):
|
|||
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):
|
||||
|
|
|
@ -6,11 +6,29 @@
|
|||
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
|
||||
<ul class="object-tools"><li><a href="{{ history_url }}" class="historylink">{% trans "History" %}</a></li>
|
||||
{% if has_absolute_url %}<li><a href="../../../r/{{ content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
|
||||
{% if not original.periodicite %}
|
||||
<li><a href="{% url "admin:eo_facture_contrat_duplicate" original.id %}">Dupliquer</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url "admin:eo_facture_facture_changelist" %}?contrat={{ original.id }}">Factures</a></li>
|
||||
{% if not original.periodicite %}
|
||||
<li><a href="{% url "admin:eo_facture_facture_add" %}?contrat={{ original.id }}&client={{ original.client.id }}&taux_tva={{ original.tva }}">Ajouter une facture</a></li>
|
||||
<li><a href="{% url "admin:eo_facture_facture_add_simple" %}?contrat={{ original.id }}">Ajouter une facture comme pourcentage du total</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block after_field_sets %}
|
||||
{% if original.periodicite and original.periodicite_debut %}
|
||||
<fieldset class="module">
|
||||
<h2>Échéances</h2>
|
||||
<div>
|
||||
<ul>
|
||||
{% for echeance in original.periodicite_echeances %}
|
||||
<li>du {{ echeance.1 }} au {{ echeance.2 }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
<ul class="object-tools">
|
||||
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
|
||||
<li><a href="{{ history_url }}" class="historylink">{% trans "History" %}</a></li>
|
||||
{% if original.client %}
|
||||
<li><a href="{% url "admin:eo_facture_client_change" original.client.id %}" class="historylink">Client</a></li>
|
||||
{% endif %}
|
||||
{% if original.contrat %}
|
||||
<li><a href="{% url "admin:eo_facture_contrat_change" original.contrat.id %}" class="historylink">Contrat</a></li>
|
||||
{% endif %}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{% load eo_facture %}
|
||||
{% if echeances %}
|
||||
<div class="module" id="echeances">
|
||||
<h2>Contrats récurrents à facturer</h2>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th class="debut">Date anniversaire</th>
|
||||
<th class="client">Client</th>
|
||||
<th class="contrat">Contrat</th>
|
||||
<th class="occurence">Échéance</th>
|
||||
<th class="periodicite">Périodicité</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for echeance in echeances %}
|
||||
<tr>
|
||||
<td class="debut">{{ echeance.debut }} - {{ echeance.fin }}</td>
|
||||
<th class="client"><a href="{% url "admin:eo_facture_client_change" echeance.contrat.client.id %}">{{ echeance.contrat.client }}</a></th>
|
||||
<td class="contrat">
|
||||
<a href="{% url "admin:eo_facture_contrat_change" echeance.contrat.id %}">
|
||||
{{ echeance.contrat.intitule }}
|
||||
<a/>
|
||||
</td>
|
||||
<td class="occurence">{{ echeance.occurrence }} / {{ echeance.contrat.periodicite_nombre_d_echeances }}</td>
|
||||
<td class="periodicite">{{ echeance.contrat.get_periodicite_display }}</td>
|
||||
<td>
|
||||
<form method="post" action="{% url "admin:eo_facture_contrat_facturer_echeance" echeance.contrat.id %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="echeance" value="{{ echeance.occurrence }}"/>
|
||||
<input type="submit" value="Facturer" style="padding: 2px 3px"/>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
|
@ -19,6 +19,7 @@ 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
|
||||
|
@ -27,6 +28,7 @@ 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
|
||||
|
@ -320,3 +322,33 @@ def payment_post_save(raw, **kwargs):
|
|||
@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
|
||||
],
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
{% echeances %}
|
||||
{% income_by_clients %}
|
||||
{% impayees %}
|
||||
{% a_facturer %}
|
||||
|
|
1
setup.py
1
setup.py
|
@ -20,6 +20,7 @@ install_requires = [
|
|||
'XStatic-Select2',
|
||||
'gadjo',
|
||||
'django-mellon',
|
||||
'python-dateutil',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -6,3 +6,4 @@ pytest-freezegun
|
|||
httmock
|
||||
django-webtest
|
||||
uwsgidecorators
|
||||
pyquery
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# barbacompta - invoicing for dummies
|
||||
# Copyright (C) 2022 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 pytest
|
||||
|
||||
|
||||
class TestLoggedIn:
|
||||
@pytest.fixture
|
||||
def app(self, app):
|
||||
# login
|
||||
response = app.get('/').follow()
|
||||
response.form['username'] = 'admin'
|
||||
response.form['password'] = 'admin'
|
||||
response.form.submit()
|
||||
return app
|
||||
|
||||
def test_contrat_recurrent_no_debut(self, app):
|
||||
response = app.get('/eo_facture/contrat/add/')
|
||||
response.form['client'].force_value('1')
|
||||
response.form['intitule'] = 'Contrat 1'
|
||||
response.form['periodicite'] = 'annuelle'
|
||||
response = response.form.submit('_continue')
|
||||
assert len(response.pyquery('.errorlist'))
|
||||
|
||||
def test_contrat_recurrent_debut_changed(self, app):
|
||||
response = app.get('/eo_facture/contrat/add/')
|
||||
response.form['client'].force_value('1')
|
||||
response.form['intitule'] = 'Contrat 1'
|
||||
response.form['periodicite'] = 'annuelle'
|
||||
response.form['periodicite_debut'] = '2018-12-01'
|
||||
response = response.form.submit('_continue').follow()
|
||||
response.form['periodicite_debut'] = '2018-12-02'
|
||||
response = response.form.submit('_continue').follow()
|
||||
|
||||
# Créer la facture de première échéance
|
||||
response = app.get('/')
|
||||
assert 'facturer-echeance' in response.form.action
|
||||
response = response.form.submit().follow()
|
||||
|
||||
# Essayons de modifier le début
|
||||
response = response.click('Contrat', index=1)
|
||||
response.form['periodicite_debut'] = '2018-12-01'
|
||||
response = response.form.submit('_continue')
|
||||
# Il y a une erreur
|
||||
assert len(response.pyquery('.errorlist.nonfield'))
|
Loading…
Reference in New Issue